const authSecurity = [{ SessionToken: [] }, { SecureSessionToken: [] }, { BearerAuth: [] }]; const adminSecurity = [{ SessionToken: [] }, { SecureSessionToken: [] }, { BearerAuth: [] }]; const jsonContent = (schema: unknown, example?: unknown) => ({ "application/json": { schema, ...(example !== undefined ? { example } : {}), }, }); const requestBody = (schema: unknown, example?: unknown, required = true) => ({ required, content: jsonContent(schema, example), }); const jsonResponse = (description: string, schema: unknown, example?: unknown) => ({ description, content: jsonContent(schema, example), }); const errorResponse = (status: string, description: string, example?: string) => [ status, jsonResponse( description, { $ref: "#/components/schemas/ErrorResponse" }, example ? { error: example } : undefined ), ]; export const openApiSpec = { openapi: "3.0.3", info: { title: "Football Next API", version: "1.0.0", description: "مستندات Swagger/OpenAPI برای تمام APIهای پروژه Football Next. این مستندات بر اساس Routeهای فعلی پروژه تهیه شده و برای توسعه، تست و تحویل به فرانت/بک قابل استفاده است.", }, servers: [ { url: process.env.NEXTAUTH_URL ?? "http://localhost:3000", description: "Current app origin", }, ], tags: [ { name: "Auth", description: "ثبت‌نام، نشست و مسیرهای مرتبط با NextAuth" }, { name: "User", description: "عملیات مرتبط با پروفایل و نشست کاربر" }, { name: "Team", description: "ساخت و مدیریت تیم فانتزی کاربر" }, { name: "Players", description: "دریافت و مدیریت بازیکنان" }, { name: "Countries", description: "دریافت و مدیریت کشورها" }, { name: "Matches", description: "دریافت و مدیریت بازی‌ها و آمار آن‌ها" }, { name: "Rounds", description: "مدیریت دورها" }, { name: "Gameweeks", description: "مدیریت هفته‌ها" }, { name: "Leaderboard", description: "رتبه‌بندی تیم‌ها" }, { name: "Upload", description: "آپلود فایل‌های تصویری" }, { name: "Payment", description: "شروع و تایید پرداخت" }, { name: "Quiz", description: "کوئیز روزانه و نتایج آن" }, { name: "Golden Cards", description: "کارت‌های طلایی کاربر" }, { name: "Admin", description: "APIهای ویژه ادمین" }, ], components: { securitySchemes: { SessionToken: { type: "apiKey", in: "cookie", name: "next-auth.session-token", description: "توکن سشن NextAuth در محیط عادی", }, SecureSessionToken: { type: "apiKey", in: "cookie", name: "__Secure-next-auth.session-token", description: "توکن سشن NextAuth در محیط HTTPS", }, BearerAuth: { type: "http", scheme: "bearer", bearerFormat: "opaque", description: "Mobile app access token returned by /api/auth/mobile/verify or /api/auth/mobile/refresh", }, }, schemas: { ErrorResponse: { type: "object", properties: { error: { type: "string", example: "Unauthorized" }, }, required: ["error"], }, SuccessResponse: { type: "object", properties: { success: { type: "boolean", example: true }, }, required: ["success"], }, Role: { type: "string", enum: ["USER", "ADMIN"], }, Position: { type: "string", enum: ["GK", "DEF", "MID", "FWD"], }, MatchStage: { type: "string", enum: ["GROUP", "ROUND_OF_16", "QUARTER_FINAL", "SEMI_FINAL", "THIRD_PLACE", "FINAL"], }, MatchStatus: { type: "string", enum: ["SCHEDULED", "LIVE", "FINISHED"], }, TeamStatus: { type: "string", enum: ["PENDING", "APPROVED", "REJECTED", "ACTIVE", "INACTIVE"], }, GoldenCardStatus: { type: "string", enum: ["SEALED", "OPENED"], }, EventType: { type: "string", enum: [ "GOAL", "ASSIST", "YELLOW_CARD", "RED_CARD", "SECOND_YELLOW", "SUBSTITUTION_IN", "SUBSTITUTION_OUT", "INJURY_NO_SUB", "CLEAN_SHEET", "PENALTY_SAVED", "PENALTY_MISSED", "OWN_GOAL", "EXTRA_TIME_BONUS", "MOTM", ], }, Group: { type: "object", properties: { id: { type: "string" }, name: { type: "string" }, }, }, Country: { type: "object", properties: { id: { type: "string" }, name: { type: "string" }, code: { type: "string" }, flagUrl: { type: "string", nullable: true }, flagImage: { type: "string", nullable: true }, confederation: { type: "string", nullable: true }, qualificationMethod: { type: "string", nullable: true }, qualificationDate: { type: "string", nullable: true }, participationHistory: { type: "string", nullable: true }, bestResult: { type: "string", nullable: true }, description: { type: "string", nullable: true }, defaultFormation: { type: "string", example: "4-3-3" }, defaultLineupPlayerIds: { type: "array", items: { type: "string" } }, defaultCaptainId: { type: "string", nullable: true }, groupId: { type: "string", nullable: true }, isEliminated: { type: "boolean" }, group: { $ref: "#/components/schemas/Group" }, }, }, Player: { type: "object", properties: { id: { type: "string" }, name: { type: "string" }, image: { type: "string", nullable: true }, position: { $ref: "#/components/schemas/Position" }, countryId: { type: "string" }, country: { $ref: "#/components/schemas/Country" }, price: { type: "number", format: "float" }, totalPoints: { type: "integer" }, isActive: { type: "boolean" }, isGoldenCardEligible: { type: "boolean" }, createdAt: { type: "string", format: "date-time" }, updatedAt: { type: "string", format: "date-time" }, }, }, MatchLineupInput: { type: "object", properties: { countryId: { type: "string" }, formation: { type: "string", example: "4-3-3" }, playerIds: { type: "array", items: { type: "string" } }, }, required: ["countryId", "formation", "playerIds"], }, MatchEvent: { type: "object", properties: { id: { type: "string" }, matchId: { type: "string" }, playerId: { type: "string" }, type: { $ref: "#/components/schemas/EventType" }, minute: { type: "integer", nullable: true }, extraInfo: { type: "string", nullable: true }, createdAt: { type: "string", format: "date-time" }, }, }, PlayerMatchStat: { type: "object", properties: { id: { type: "string" }, playerId: { type: "string" }, matchId: { type: "string" }, goals: { type: "integer" }, assists: { type: "integer" }, yellowCards: { type: "integer" }, redCards: { type: "integer" }, minutesPlayed: { type: "integer" }, cleanSheet: { type: "boolean" }, penaltySaved: { type: "integer" }, penaltyMissed: { type: "integer" }, ownGoals: { type: "integer" }, isMotm: { type: "boolean" }, extraTimeBonus: { type: "integer" }, points: { type: "integer" }, }, }, Match: { type: "object", properties: { id: { type: "string" }, homeTeamId: { type: "string" }, awayTeamId: { type: "string" }, homeTeam: { $ref: "#/components/schemas/Country" }, awayTeam: { $ref: "#/components/schemas/Country" }, homeScore: { type: "integer", nullable: true }, awayScore: { type: "integer", nullable: true }, stage: { $ref: "#/components/schemas/MatchStage" }, status: { $ref: "#/components/schemas/MatchStatus" }, matchDate: { type: "string", format: "date-time" }, matchDatePersian: { type: "string", nullable: true }, stadium: { type: "string", nullable: true }, city: { type: "string", nullable: true }, referee: { type: "string", nullable: true }, assistant1: { type: "string", nullable: true }, assistant2: { type: "string", nullable: true }, fourthOfficial: { type: "string", nullable: true }, attendance: { type: "integer", nullable: true }, weather: { type: "string", nullable: true }, description: { type: "string", nullable: true }, roundId: { type: "string", nullable: true }, playerStats: { type: "array", items: { $ref: "#/components/schemas/PlayerMatchStat" }, }, createdAt: { type: "string", format: "date-time" }, }, }, Round: { type: "object", properties: { id: { type: "string" }, number: { type: "integer" }, name: { type: "string" }, isActive: { type: "boolean" }, deadline: { type: "string", format: "date-time" }, createdAt: { type: "string", format: "date-time" }, }, }, Gameweek: { type: "object", properties: { id: { type: "string" }, number: { type: "integer" }, name: { type: "string" }, isActive: { type: "boolean" }, deadline: { type: "string", format: "date-time" }, createdAt: { type: "string", format: "date-time" }, }, }, TeamPlayer: { type: "object", properties: { teamId: { type: "string" }, playerId: { type: "string" }, player: { $ref: "#/components/schemas/Player" }, isCaptain: { type: "boolean" }, isViceCaptain: { type: "boolean" }, isBench: { type: "boolean" }, positionIndex: { type: "integer" }, }, }, Team: { type: "object", properties: { id: { type: "string" }, name: { type: "string" }, userId: { type: "string" }, budget: { type: "number", format: "float" }, totalPoints: { type: "integer" }, formation: { type: "string" }, status: { $ref: "#/components/schemas/TeamStatus" }, createdAt: { type: "string", format: "date-time" }, players: { type: "array", items: { $ref: "#/components/schemas/TeamPlayer" }, }, }, }, LeaderboardEntry: { type: "object", properties: { rank: { type: "integer" }, teamName: { type: "string" }, userName: { type: "string" }, totalPoints: { type: "integer" }, budget: { type: "number", format: "float" }, }, }, FantasyNews: { type: "object", properties: { id: { type: "string" }, icon: { type: "string" }, title: { type: "string" }, description: { type: "string" }, newsTime: { type: "string", format: "date-time" }, createdAt: { type: "string", format: "date-time" }, updatedAt: { type: "string", format: "date-time", nullable: true }, }, }, FantasyNewsListResponse: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/FantasyNews" }, }, }, }, FantasyNewsCreateRequest: { type: "object", properties: { icon: { type: "string", example: "info" }, title: { type: "string", example: "Lineup update" }, description: { type: "string", example: "Confirmed team news before kickoff." }, newsTime: { type: "string", format: "date-time", example: "2026-06-10T12:00:00.000Z" }, }, required: ["icon", "title", "description", "newsTime"], }, PaymentRequestPayload: { type: "object", properties: { packageId: { type: "string" }, }, required: ["packageId"], }, PaymentRequestResponse: { type: "object", properties: { paymentUrl: { type: "string", format: "uri" }, }, }, UploadPlayerImageResponse: { type: "object", properties: { success: { type: "boolean" }, fileName: { type: "string" }, url: { type: "string" }, }, }, SessionInfo: { type: "object", properties: { user: { type: "object", properties: { name: { type: "string", nullable: true }, email: { type: "string", nullable: true }, image: { type: "string", nullable: true }, role: { $ref: "#/components/schemas/Role" }, id: { type: "string" }, }, }, expires: { type: "string", format: "date-time" }, }, }, DailyQuizQuestionPublic: { type: "object", properties: { id: { type: "string" }, questionText: { type: "string" }, options: { type: "array", items: { type: "string" } }, order: { type: "integer" }, }, }, DailyQuiz: { type: "object", properties: { id: { type: "string" }, date: { type: "string", format: "date" }, windowStart: { type: "string", format: "date-time" }, windowEnd: { type: "string", format: "date-time" }, goldWinnersCount: { type: "integer" }, silverWinnersCount: { type: "integer" }, bronzeWinnersCount: { type: "integer" }, isProcessed: { type: "boolean" }, createdAt: { type: "string", format: "date-time" }, isActive: { type: "boolean" }, questions: { type: "array", items: { $ref: "#/components/schemas/DailyQuizQuestionPublic" }, }, }, }, GoldenCard: { type: "object", properties: { id: { type: "string" }, userId: { type: "string" }, playerId: { type: "string" }, status: { $ref: "#/components/schemas/GoldenCardStatus" }, acquiredDate: { type: "string", format: "date-time" }, openedAt: { type: "string", format: "date-time", nullable: true }, player: { $ref: "#/components/schemas/Player" }, }, }, RegisterRequest: { type: "object", properties: { name: { type: "string", nullable: true }, email: { type: "string", format: "email" }, password: { type: "string", format: "password" }, }, required: ["email", "password"], }, RegisterResponse: { type: "object", properties: { id: { type: "string" }, }, }, MobileOtpRequestCodeRequest: { type: "object", properties: { phone: { type: "string", example: "09123456789" }, }, required: ["phone"], }, MobileOtpRequestCodeResponse: { type: "object", properties: { ok: { type: "boolean", example: true }, expiresIn: { type: "integer", example: 120 }, }, }, MobileOtpVerifyRequest: { type: "object", properties: { phone: { type: "string", example: "09123456789" }, code: { type: "string", example: "123456" }, name: { type: "string", nullable: true, example: "Ali" }, }, required: ["phone", "code"], }, MobileOtpVerifyResponse: { type: "object", properties: { accessToken: { type: "string" }, token: { type: "string" }, tokenType: { type: "string", example: "Bearer" }, expiresIn: { type: "integer", example: 900 }, expiresAt: { type: "string", format: "date-time" }, refreshToken: { type: "string" }, refreshExpiresIn: { type: "integer", example: 2592000 }, refreshExpiresAt: { type: "string", format: "date-time" }, user: { type: "object", properties: { id: { type: "string" }, name: { type: "string", nullable: true }, phone: { type: "string", nullable: true }, role: { $ref: "#/components/schemas/Role" }, }, }, }, }, MobileRefreshRequest: { type: "object", properties: { refreshToken: { type: "string" }, }, required: ["refreshToken"], }, MobileLogoutRequest: { type: "object", properties: { refreshToken: { type: "string" }, }, }, CredentialsLoginRequest: { type: "object", properties: { email: { type: "string", format: "email" }, password: { type: "string", format: "password" }, redirect: { type: "boolean", default: false }, callbackUrl: { type: "string", nullable: true }, }, required: ["email", "password"], }, TeamCreateRequest: { type: "object", properties: { name: { type: "string" }, formation: { type: "string", example: "4-3-3" }, }, required: ["name"], }, TeamPlayerAddRequest: { type: "object", properties: { playerId: { type: "string" }, isBench: { type: "boolean", default: false }, }, required: ["playerId"], }, TeamPlayerRemoveRequest: { type: "object", properties: { playerId: { type: "string" }, }, required: ["playerId"], }, TeamFormationRequest: { type: "object", properties: { formation: { type: "string", enum: ["4-3-3", "4-4-2", "4-5-1", "3-5-2", "3-4-3", "5-3-2", "5-4-1"], }, }, required: ["formation"], }, TeamCaptainRequest: { type: "object", properties: { playerId: { type: "string" }, type: { type: "string", enum: ["captain", "viceCaptain"] }, }, required: ["playerId", "type"], }, ProfileUpdateRequest: { type: "object", properties: { name: { type: "string" }, }, required: ["name"], }, ManualPlayerStatInput: { type: "object", properties: { playerId: { type: "string" }, goals: { type: "integer" }, assists: { type: "integer" }, yellowCards: { type: "integer" }, redCards: { type: "integer" }, minutesPlayed: { type: "integer" }, cleanSheet: { type: "boolean" }, }, required: ["playerId", "goals", "assists", "yellowCards", "redCards", "minutesPlayed", "cleanSheet"], }, QuizSubmitRequest: { type: "object", properties: { quizId: { type: "string" }, answers: { type: "array", items: { type: "integer" } }, }, required: ["quizId", "answers"], }, AdminQuizCreateRequest: { type: "object", properties: { date: { type: "string", format: "date" }, windowStart: { type: "string", format: "date-time" }, windowEnd: { type: "string", format: "date-time" }, goldWinnersCount: { type: "integer", default: 1 }, silverWinnersCount: { type: "integer", default: 0 }, bronzeWinnersCount: { type: "integer", default: 0 }, questions: { type: "array", items: { type: "object", properties: { questionText: { type: "string" }, options: { type: "array", items: { type: "string" } }, correctAnswer: { type: "integer" }, }, required: ["questionText", "options", "correctAnswer"], }, }, }, required: ["date", "windowStart", "windowEnd", "goldWinnersCount", "silverWinnersCount", "bronzeWinnersCount", "questions"], }, AdminTeamStatusUpdateRequest: { type: "object", properties: { status: { $ref: "#/components/schemas/TeamStatus" }, }, required: ["status"], }, ScoringRuleInput: { type: "object", properties: { position: { $ref: "#/components/schemas/Position" }, eventType: { $ref: "#/components/schemas/EventType" }, points: { type: "integer" }, }, required: ["position", "eventType", "points"], }, MatchEventCreateRequest: { type: "object", properties: { playerId: { type: "string" }, type: { $ref: "#/components/schemas/EventType" }, minute: { type: "integer", nullable: true }, extraInfo: { type: "string", nullable: true }, }, required: ["playerId", "type"], }, }, }, paths: { "/api/auth/register": { post: { tags: ["Auth"], summary: "ثبت‌نام کاربر جدید", requestBody: requestBody( { $ref: "#/components/schemas/RegisterRequest" }, { name: "Ali", email: "ali@example.com", password: "123456" } ), responses: Object.fromEntries([ ["201", jsonResponse("کاربر ساخته شد", { $ref: "#/components/schemas/RegisterResponse" }, { id: "clx123" })], errorResponse("400", "ورودی نامعتبر", "این ایمیل قبلاً ثبت شده"), ]), }, }, "/api/auth/mobile/request-code": { post: { tags: ["Auth"], summary: "Request mobile OTP login code", requestBody: requestBody( { $ref: "#/components/schemas/MobileOtpRequestCodeRequest" }, { phone: "09123456789" } ), responses: Object.fromEntries([ ["200", jsonResponse("OTP code sent", { $ref: "#/components/schemas/MobileOtpRequestCodeResponse" })], errorResponse("400", "Invalid mobile phone", "Invalid mobile phone"), errorResponse("429", "OTP request rate limited", "Please wait before requesting a new code"), errorResponse("502", "SMS provider failed", "SMS send failed"), ]), }, }, "/api/auth/mobile/verify": { post: { tags: ["Auth"], summary: "Verify mobile OTP login code", requestBody: requestBody( { $ref: "#/components/schemas/MobileOtpVerifyRequest" }, { phone: "09123456789", code: "123456", name: "Ali" } ), responses: Object.fromEntries([ ["200", jsonResponse("OTP verified", { $ref: "#/components/schemas/MobileOtpVerifyResponse" })], errorResponse("400", "Invalid or expired OTP", "Invalid or expired OTP"), ]), }, }, "/api/auth/mobile/refresh": { post: { tags: ["Auth"], summary: "Rotate refresh token and issue a new mobile access token", requestBody: requestBody( { $ref: "#/components/schemas/MobileRefreshRequest" }, { refreshToken: "refresh_token_value" } ), responses: Object.fromEntries([ ["200", jsonResponse("Token refreshed", { $ref: "#/components/schemas/MobileOtpVerifyResponse" })], errorResponse("400", "Missing refresh token", "refreshToken is required"), errorResponse("401", "Invalid, expired, or revoked refresh token", "Invalid refresh token"), ]), }, }, "/api/auth/mobile/logout": { post: { tags: ["Auth"], summary: "Revoke mobile refresh token family and delete current access token", security: authSecurity, requestBody: requestBody( { $ref: "#/components/schemas/MobileLogoutRequest" }, { refreshToken: "refresh_token_value" }, false ), responses: { "200": jsonResponse("Logged out", { type: "object", properties: { ok: { type: "boolean", example: true }, }, }, { ok: true }), }, }, }, "/api/auth/callback/credentials": { post: { tags: ["Auth"], summary: "ورود با ایمیل و رمز عبور", description: "مسیر استاندارد NextAuth برای ورود. در فرانت معمولاً از `signIn('credentials')` استفاده می‌شود. در Swagger برای تست بهتر است از همان مرورگری استفاده شود که Session Cookie را نگه می‌دارد.", requestBody: requestBody( { $ref: "#/components/schemas/CredentialsLoginRequest" }, { email: "admin@example.com", password: "123456", redirect: false } ), responses: { "200": { description: "ورود موفق یا پاسخ استاندارد NextAuth", content: { "application/json": { schema: { type: "object", additionalProperties: true, }, }, }, }, }, }, }, "/api/auth/session": { get: { tags: ["Auth"], summary: "دریافت سشن جاری", responses: { "200": jsonResponse( "اطلاعات نشست", { oneOf: [{ $ref: "#/components/schemas/SessionInfo" }, { type: "null" }], } ), }, }, }, "/api/auth/csrf": { get: { tags: ["Auth"], summary: "دریافت CSRF Token برای NextAuth", responses: { "200": jsonResponse( "توکن CSRF", { type: "object", properties: { csrfToken: { type: "string" }, }, } ), }, }, }, "/api/auth/signout": { post: { tags: ["Auth"], summary: "خروج کاربر", security: authSecurity, responses: { "200": { description: "خروج موفق", }, }, }, }, "/api/user/profile": { put: { tags: ["User"], summary: "ویرایش نام کاربر", security: authSecurity, requestBody: requestBody( { $ref: "#/components/schemas/ProfileUpdateRequest" }, { name: "Ali Rezaei" } ), responses: Object.fromEntries([ ["200", jsonResponse("پروفایل به‌روزرسانی شد", { type: "object", properties: { name: { type: "string", nullable: true } } })], errorResponse("401", "نیازمند ورود", "Unauthorized"), ]), }, }, "/api/test-session": { get: { tags: ["User"], summary: "بررسی سشن و وضعیت کاربر در دیتابیس", security: authSecurity, responses: Object.fromEntries([ [ "200", jsonResponse("وضعیت سشن", { type: "object", additionalProperties: true, }), ], errorResponse("401", "بدون سشن", "No session"), ]), }, }, "/api/team": { get: { tags: ["Team"], summary: "دریافت تیم کاربر جاری", security: authSecurity, responses: Object.fromEntries([ ["200", jsonResponse("تیم کاربر", { oneOf: [{ $ref: "#/components/schemas/Team" }, { type: "null" }] })], errorResponse("401", "نیازمند ورود", "Unauthorized"), ]), }, post: { tags: ["Team"], summary: "ساخت تیم جدید برای کاربر", security: authSecurity, requestBody: requestBody( { $ref: "#/components/schemas/TeamCreateRequest" }, { name: "Dream FC", formation: "4-3-3" } ), responses: Object.fromEntries([ ["201", jsonResponse("تیم ساخته شد", { $ref: "#/components/schemas/Team" })], errorResponse("400", "خطای اعتبارسنجی", "Team already exists"), errorResponse("401", "نیازمند ورود", "Unauthorized"), errorResponse("404", "کاربر پیدا نشد", "User not found"), ]), }, }, "/api/team/players": { post: { tags: ["Team"], summary: "افزودن بازیکن به تیم", security: authSecurity, requestBody: requestBody( { $ref: "#/components/schemas/TeamPlayerAddRequest" }, { playerId: "player_123", isBench: false } ), responses: Object.fromEntries([ ["201", jsonResponse("بازیکن به تیم اضافه شد", { $ref: "#/components/schemas/TeamPlayer" })], errorResponse("400", "خطای اعتبارسنجی", "بودجه کافی نیست"), errorResponse("401", "نیازمند ورود", "Unauthorized"), errorResponse("404", "بازیکن پیدا نشد", "بازیکن پیدا نشد"), ]), }, delete: { tags: ["Team"], summary: "حذف بازیکن از تیم", security: authSecurity, requestBody: requestBody( { $ref: "#/components/schemas/TeamPlayerRemoveRequest" }, { playerId: "player_123" } ), responses: Object.fromEntries([ ["200", jsonResponse("بازیکن حذف شد", { $ref: "#/components/schemas/SuccessResponse" })], errorResponse("401", "نیازمند ورود", "Unauthorized"), errorResponse("404", "تیم پیدا نشد", "تیم پیدا نشد"), ]), }, }, "/api/team/formation": { put: { tags: ["Team"], summary: "تغییر ترکیب تیم", security: authSecurity, requestBody: requestBody( { $ref: "#/components/schemas/TeamFormationRequest" }, { formation: "4-4-2" } ), responses: Object.fromEntries([ ["200", jsonResponse("ترکیب به‌روزرسانی شد", { $ref: "#/components/schemas/Team" })], errorResponse("400", "ترکیب نامعتبر یا ناسازگار", "ترکیب نامعتبر"), errorResponse("401", "نیازمند ورود", "Unauthorized"), errorResponse("404", "تیم پیدا نشد", "تیم پیدا نشد"), ]), }, }, "/api/team/captain": { put: { tags: ["Team"], summary: "تعیین کاپیتان یا نایب کاپیتان", security: authSecurity, requestBody: requestBody( { $ref: "#/components/schemas/TeamCaptainRequest" }, { playerId: "player_123", type: "captain" } ), responses: Object.fromEntries([ ["200", jsonResponse("بروزرسانی موفق", { $ref: "#/components/schemas/SuccessResponse" })], errorResponse("401", "نیازمند ورود", "Unauthorized"), errorResponse("404", "تیم پیدا نشد", "تیم پیدا نشد"), ]), }, }, "/api/team/submit": { post: { tags: ["Team"], summary: "ثبت نهایی تیم", security: authSecurity, responses: Object.fromEntries([ ["200", jsonResponse("تیم نهایی شد", { $ref: "#/components/schemas/Team" })], errorResponse("400", "ترکیب تیم نامعتبر است", "تیم باید دقیقاً ۱۵ بازیکن داشته باشد"), errorResponse("401", "نیازمند ورود", "Unauthorized"), errorResponse("404", "تیم پیدا نشد", "تیم پیدا نشد"), ]), }, }, "/api/players": { get: { tags: ["Players"], summary: "لیست بازیکنان", parameters: [ { in: "query", name: "position", schema: { $ref: "#/components/schemas/Position" }, required: false }, { in: "query", name: "countryId", schema: { type: "string" }, required: false }, ], responses: { "200": jsonResponse("لیست بازیکنان", { type: "array", items: { $ref: "#/components/schemas/Player" } }), }, }, post: { tags: ["Players"], summary: "ایجاد بازیکن جدید", security: adminSecurity, requestBody: requestBody( { type: "object", additionalProperties: true, properties: { name: { type: "string" }, position: { $ref: "#/components/schemas/Position" }, countryId: { type: "string" }, price: { type: "number" }, image: { type: "string", nullable: true }, }, }, { name: "Lionel Messi", position: "FWD", countryId: "country_1", price: 12.5 } ), responses: Object.fromEntries([ ["201", jsonResponse("بازیکن ایجاد شد", { $ref: "#/components/schemas/Player" })], errorResponse("401", "نیازمند دسترسی ادمین", "Unauthorized"), ]), }, }, "/api/players/{id}": { put: { tags: ["Players"], summary: "ویرایش بازیکن", security: adminSecurity, parameters: [{ in: "path", name: "id", required: true, schema: { type: "string" } }], requestBody: requestBody( { type: "object", additionalProperties: true, }, { price: 11.5, isActive: true } ), responses: Object.fromEntries([ ["200", jsonResponse("بازیکن به‌روزرسانی شد", { $ref: "#/components/schemas/Player" })], errorResponse("401", "نیازمند دسترسی ادمین", "Unauthorized"), ]), }, delete: { tags: ["Players"], summary: "حذف بازیکن", security: adminSecurity, parameters: [{ in: "path", name: "id", required: true, schema: { type: "string" } }], responses: Object.fromEntries([ ["200", jsonResponse("بازیکن حذف شد", { $ref: "#/components/schemas/SuccessResponse" })], errorResponse("401", "نیازمند دسترسی ادمین", "Unauthorized"), ]), }, }, "/api/admin/players/{id}/golden-toggle": { patch: { tags: ["Admin", "Golden Cards"], summary: "فعال/غیرفعال کردن صلاحیت Golden Card برای بازیکن", security: adminSecurity, parameters: [{ in: "path", name: "id", required: true, schema: { type: "string" } }], responses: Object.fromEntries([ [ "200", jsonResponse("وضعیت تغییر کرد", { type: "object", properties: { isGoldenCardEligible: { type: "boolean" }, }, }), ], errorResponse("401", "نیازمند دسترسی ادمین", "Unauthorized"), errorResponse("404", "بازیکن پیدا نشد", "Player not found"), ]), }, }, "/api/countries": { get: { tags: ["Countries"], summary: "لیست کشورها", responses: { "200": jsonResponse("لیست کشورها", { type: "array", items: { $ref: "#/components/schemas/Country" } }), }, }, post: { tags: ["Countries"], summary: "ایجاد کشور جدید", security: adminSecurity, requestBody: requestBody( { type: "object", additionalProperties: true, properties: { name: { type: "string" }, code: { type: "string" }, defaultFormation: { type: "string" }, }, }, { name: "Argentina", code: "ARG", defaultFormation: "4-3-3" } ), responses: Object.fromEntries([ ["201", jsonResponse("کشور ایجاد شد", { $ref: "#/components/schemas/Country" })], errorResponse("401", "نیازمند دسترسی ادمین", "Unauthorized"), ]), }, }, "/api/countries/{id}": { put: { tags: ["Countries"], summary: "ویرایش کشور", security: adminSecurity, parameters: [{ in: "path", name: "id", required: true, schema: { type: "string" } }], requestBody: requestBody({ type: "object", additionalProperties: true }, { isEliminated: false }), responses: Object.fromEntries([ ["200", jsonResponse("کشور به‌روزرسانی شد", { $ref: "#/components/schemas/Country" })], errorResponse("401", "نیازمند دسترسی ادمین", "Unauthorized"), ]), }, delete: { tags: ["Countries"], summary: "حذف کشور", security: adminSecurity, parameters: [{ in: "path", name: "id", required: true, schema: { type: "string" } }], responses: Object.fromEntries([ ["200", jsonResponse("کشور حذف شد", { $ref: "#/components/schemas/SuccessResponse" })], errorResponse("401", "نیازمند دسترسی ادمین", "Unauthorized"), ]), }, }, "/api/matches": { get: { tags: ["Matches"], summary: "لیست بازی‌ها", responses: { "200": jsonResponse("لیست بازی‌ها", { type: "array", items: { $ref: "#/components/schemas/Match" } }), }, }, post: { tags: ["Matches"], summary: "ایجاد بازی جدید", security: adminSecurity, requestBody: requestBody( { type: "object", additionalProperties: true, properties: { homeTeamId: { type: "string" }, awayTeamId: { type: "string" }, stage: { $ref: "#/components/schemas/MatchStage" }, status: { $ref: "#/components/schemas/MatchStatus" }, matchDate: { type: "string", format: "date-time" }, roundId: { type: "string", nullable: true }, }, }, { homeTeamId: "country_1", awayTeamId: "country_2", stage: "GROUP", status: "SCHEDULED", matchDate: "2026-06-10T19:00:00.000Z", } ), responses: Object.fromEntries([ ["201", jsonResponse("بازی ایجاد شد", { $ref: "#/components/schemas/Match" })], errorResponse("401", "نیازمند دسترسی ادمین", "Unauthorized"), ]), }, }, "/api/matches/{id}": { get: { tags: ["Matches"], summary: "جزئیات یک بازی", parameters: [{ in: "path", name: "id", required: true, schema: { type: "string" } }], responses: Object.fromEntries([ ["200", jsonResponse("جزئیات بازی", { $ref: "#/components/schemas/Match" })], errorResponse("404", "بازی پیدا نشد", "Not found"), ]), }, put: { tags: ["Matches"], summary: "ویرایش بازی", security: adminSecurity, parameters: [{ in: "path", name: "id", required: true, schema: { type: "string" } }], requestBody: requestBody({ type: "object", additionalProperties: true }, { status: "LIVE", homeScore: 1, awayScore: 0 }), responses: Object.fromEntries([ ["200", jsonResponse("بازی به‌روزرسانی شد", { $ref: "#/components/schemas/Match" })], errorResponse("401", "نیازمند دسترسی ادمین", "Unauthorized"), ]), }, delete: { tags: ["Matches"], summary: "حذف بازی", security: adminSecurity, parameters: [{ in: "path", name: "id", required: true, schema: { type: "string" } }], responses: Object.fromEntries([ ["200", jsonResponse("بازی حذف شد", { $ref: "#/components/schemas/SuccessResponse" })], errorResponse("401", "نیازمند دسترسی ادمین", "Unauthorized"), ]), }, }, "/api/matches/{id}/stats": { post: { tags: ["Matches"], summary: "ثبت دستی آمار بازیکنان یک بازی", description: "این مسیر برای ثبت مستقیم آمار بازیکنان و محاسبه امتیاز آن‌ها استفاده می‌شود.", security: adminSecurity, parameters: [{ in: "path", name: "id", required: true, schema: { type: "string" } }], requestBody: requestBody( { type: "array", items: { $ref: "#/components/schemas/ManualPlayerStatInput" }, }, [ { playerId: "player_1", goals: 1, assists: 0, yellowCards: 0, redCards: 0, minutesPlayed: 90, cleanSheet: false, }, ] ), responses: Object.fromEntries([ ["200", jsonResponse("آمار ذخیره شد", { type: "array", items: { $ref: "#/components/schemas/PlayerMatchStat" } })], errorResponse("401", "نیازمند دسترسی ادمین", "Unauthorized"), ]), }, }, "/api/admin/matches/{id}/lineup": { post: { tags: ["Admin", "Matches"], summary: "ثبت ترکیب دو تیم برای یک بازی", security: adminSecurity, parameters: [{ in: "path", name: "id", required: true, schema: { type: "string" } }], requestBody: requestBody( { type: "array", items: { $ref: "#/components/schemas/MatchLineupInput" }, }, [ { countryId: "country_1", formation: "4-3-3", playerIds: ["p1", "p2"] }, { countryId: "country_2", formation: "4-4-2", playerIds: ["p3", "p4"] }, ] ), responses: Object.fromEntries([ ["200", jsonResponse("ترکیب‌ها ذخیره شدند", { $ref: "#/components/schemas/SuccessResponse" })], errorResponse("401", "نیازمند دسترسی ادمین", "Unauthorized"), ]), }, }, "/api/admin/matches/{id}/events": { post: { tags: ["Admin", "Matches"], summary: "ثبت رویداد بازی", security: adminSecurity, parameters: [{ in: "path", name: "id", required: true, schema: { type: "string" } }], requestBody: requestBody( { $ref: "#/components/schemas/MatchEventCreateRequest" }, { playerId: "player_1", type: "GOAL", minute: 35, extraInfo: "Right foot" } ), responses: Object.fromEntries([ ["201", jsonResponse("رویداد ذخیره شد", { $ref: "#/components/schemas/MatchEvent" })], errorResponse("401", "نیازمند دسترسی ادمین", "Unauthorized"), ]), }, }, "/api/admin/matches/{id}/events/{eventId}": { delete: { tags: ["Admin", "Matches"], summary: "حذف رویداد بازی", security: adminSecurity, parameters: [ { in: "path", name: "id", required: true, schema: { type: "string" } }, { in: "path", name: "eventId", required: true, schema: { type: "string" } }, ], responses: Object.fromEntries([ ["200", jsonResponse("رویداد حذف شد", { $ref: "#/components/schemas/SuccessResponse" })], errorResponse("401", "نیازمند دسترسی ادمین", "Unauthorized"), ]), }, }, "/api/admin/matches/{id}/calc-points": { post: { tags: ["Admin", "Matches"], summary: "محاسبه امتیاز بازیکنان و تیم‌ها از روی رویدادهای ثبت‌شده", security: adminSecurity, parameters: [{ in: "path", name: "id", required: true, schema: { type: "string" } }], responses: Object.fromEntries([ [ "200", jsonResponse("محاسبه انجام شد", { type: "object", properties: { calculated: { type: "integer" }, }, }), ], errorResponse("401", "نیازمند دسترسی ادمین", "Unauthorized"), ]), }, }, "/api/rounds": { get: { tags: ["Rounds"], summary: "لیست دورها", responses: { "200": jsonResponse("لیست دورها", { type: "array", items: { $ref: "#/components/schemas/Round" } }), }, }, post: { tags: ["Rounds"], summary: "ایجاد دور جدید", security: adminSecurity, requestBody: requestBody( { type: "object", properties: { number: { type: "integer" }, name: { type: "string" }, deadline: { type: "string", format: "date-time" }, }, required: ["number", "name", "deadline"], }, { number: 1, name: "Round 1", deadline: "2026-06-10T12:00:00.000Z" } ), responses: Object.fromEntries([ ["201", jsonResponse("دور ایجاد شد", { $ref: "#/components/schemas/Round" })], errorResponse("400", "شماره دور تکراری است", "این شماره دور قبلاً ثبت شده"), errorResponse("401", "نیازمند دسترسی ادمین", "Unauthorized"), ]), }, put: { tags: ["Rounds"], summary: "ویرایش دور", security: adminSecurity, requestBody: requestBody( { type: "object", properties: { id: { type: "string" }, number: { type: "integer" }, name: { type: "string" }, deadline: { type: "string", format: "date-time" }, }, required: ["id", "number", "name", "deadline"], } ), responses: Object.fromEntries([ ["200", jsonResponse("دور به‌روزرسانی شد", { $ref: "#/components/schemas/Round" })], errorResponse("401", "نیازمند دسترسی ادمین", "Unauthorized"), ]), }, delete: { tags: ["Rounds"], summary: "حذف دور", security: adminSecurity, requestBody: requestBody( { type: "object", properties: { id: { type: "string" }, }, required: ["id"], }, { id: "round_1" } ), responses: Object.fromEntries([ ["200", jsonResponse("دور حذف شد", { $ref: "#/components/schemas/SuccessResponse" })], errorResponse("400", "این دور دارای بازی است", "این دور دارای بازی است و قابل حذف نیست"), errorResponse("401", "نیازمند دسترسی ادمین", "Unauthorized"), ]), }, }, "/api/rounds/{id}/activate": { post: { tags: ["Rounds"], summary: "فعال/غیرفعال کردن یک دور", security: adminSecurity, parameters: [{ in: "path", name: "id", required: true, schema: { type: "string" } }], responses: Object.fromEntries([ ["200", jsonResponse("وضعیت دور تغییر کرد", { $ref: "#/components/schemas/Round" })], errorResponse("401", "نیازمند دسترسی ادمین", "Unauthorized"), errorResponse("404", "دور پیدا نشد", "Round not found"), ]), }, }, "/api/gameweeks": { get: { tags: ["Gameweeks"], summary: "لیست هفته‌ها", responses: { "200": jsonResponse("لیست هفته‌ها", { type: "array", items: { $ref: "#/components/schemas/Gameweek" } }), }, }, post: { tags: ["Gameweeks"], summary: "ایجاد هفته جدید", security: adminSecurity, requestBody: requestBody( { type: "object", additionalProperties: true, properties: { number: { type: "integer" }, name: { type: "string" }, deadline: { type: "string", format: "date-time" }, }, }, { number: 1, name: "Gameweek 1", deadline: "2026-06-10T12:00:00.000Z" } ), responses: Object.fromEntries([ ["201", jsonResponse("هفته ایجاد شد", { $ref: "#/components/schemas/Gameweek" })], errorResponse("401", "نیازمند دسترسی ادمین", "Unauthorized"), ]), }, }, "/api/gameweeks/{id}/activate": { post: { tags: ["Gameweeks"], summary: "فعال کردن یک هفته", security: adminSecurity, parameters: [{ in: "path", name: "id", required: true, schema: { type: "string" } }], responses: Object.fromEntries([ ["200", jsonResponse("هفته فعال شد", { $ref: "#/components/schemas/Gameweek" })], errorResponse("401", "نیازمند دسترسی ادمین", "Unauthorized"), ]), }, }, "/api/leaderboard": { get: { tags: ["Leaderboard"], summary: "دریافت جدول رتبه‌بندی", responses: { "200": jsonResponse( "لیست 50 تیم برتر", { type: "array", items: { $ref: "#/components/schemas/LeaderboardEntry" } } ), }, }, }, "/api/fantasy-news": { get: { tags: ["Fantasy News"], summary: "Get fantasy news", parameters: [ { in: "header", name: "x-news-mode", required: false, schema: { type: "string", enum: ["latest"] }, description: "Use latest to return the latest 4 summarized items", }, { in: "header", name: "x-news-summary", required: false, schema: { type: "string", enum: ["true"] }, description: "Use true to return the latest 4 summarized items", }, ], responses: { "200": jsonResponse("Fantasy news list", { $ref: "#/components/schemas/FantasyNewsListResponse" }), }, }, post: { tags: ["Fantasy News"], summary: "Create fantasy news", security: adminSecurity, requestBody: requestBody( { $ref: "#/components/schemas/FantasyNewsCreateRequest" }, { icon: "info", title: "Lineup update", description: "Confirmed team news before kickoff.", newsTime: "2026-06-10T12:00:00.000Z", } ), responses: Object.fromEntries([ ["201", jsonResponse("Fantasy news created", { $ref: "#/components/schemas/FantasyNews" })], errorResponse("400", "Invalid fantasy news payload", "Invalid fantasy news payload"), errorResponse("401", "Admin access required", "Unauthorized"), ]), }, }, "/api/upload/player-image": { post: { tags: ["Upload"], summary: "آپلود تصویر بازیکن", requestBody: { required: true, content: { "multipart/form-data": { schema: { type: "object", properties: { file: { type: "string", format: "binary", }, }, required: ["file"], }, }, }, }, responses: Object.fromEntries([ ["200", jsonResponse("فایل آپلود شد", { $ref: "#/components/schemas/UploadPlayerImageResponse" })], errorResponse("400", "فایل نامعتبر", "فایلی انتخاب نشده است"), errorResponse("500", "خطای داخلی سرور", "خطا در آپلود فایل"), ]), }, }, "/api/payment/request": { post: { tags: ["Payment"], summary: "ساخت درخواست پرداخت زرین‌پال", security: authSecurity, requestBody: requestBody( { $ref: "#/components/schemas/PaymentRequestPayload" }, { packageId: "pkg_123" } ), responses: Object.fromEntries([ ["200", jsonResponse("آدرس پرداخت ایجاد شد", { $ref: "#/components/schemas/PaymentRequestResponse" })], errorResponse("400", "خطا در ساخت پرداخت", "خطا در اتصال به درگاه"), errorResponse("401", "نیازمند ورود", "Unauthorized"), errorResponse("404", "پکیج پیدا نشد", "پکیج پیدا نشد"), ]), }, }, "/api/payment/verify": { get: { tags: ["Payment"], summary: "تایید پرداخت و ریدایرکت به فروشگاه", description: "این Endpoint توسط زرین‌پال فراخوانی می‌شود و در نهایت کاربر را به `/shop` با query string مناسب منتقل می‌کند.", parameters: [ { in: "query", name: "Authority", required: false, schema: { type: "string" } }, { in: "query", name: "Status", required: false, schema: { type: "string", example: "OK" } }, ], responses: { "307": { description: "ریدایرکت به /shop با وضعیت success, failed, cancelled یا error", }, }, }, }, "/api/quiz": { get: { tags: ["Quiz"], summary: "دریافت کوئیز روز جاری", responses: { "200": jsonResponse( "کوئیز روز یا null", { oneOf: [{ $ref: "#/components/schemas/DailyQuiz" }, { type: "null" }], } ), }, }, }, "/api/quiz/submit": { post: { tags: ["Quiz"], summary: "ارسال پاسخ‌های کوئیز", security: authSecurity, requestBody: requestBody( { $ref: "#/components/schemas/QuizSubmitRequest" }, { quizId: "quiz_123", answers: [1, 2, 0] } ), responses: Object.fromEntries([ [ "200", jsonResponse("نتیجه ثبت شد", { type: "object", properties: { score: { type: "integer" }, correct: { type: "integer" }, total: { type: "integer" }, submission: { type: "object", additionalProperties: true }, }, }), ], errorResponse("400", "خطای ارسال یا خارج از بازه", "قبلاً شرکت کرده‌اید"), errorResponse("401", "نیازمند ورود", "Unauthorized"), errorResponse("404", "کوئیز پیدا نشد", "Quiz not found"), ]), }, }, "/api/quiz/my-results": { get: { tags: ["Quiz"], summary: "دریافت نتایج کوئیزهای کاربر", security: authSecurity, responses: Object.fromEntries([ ["200", jsonResponse("نتایج کاربر", { type: "array", items: { type: "object", additionalProperties: true } })], errorResponse("401", "نیازمند ورود", "Unauthorized"), ]), }, }, "/api/admin/quiz": { get: { tags: ["Admin", "Quiz"], summary: "لیست تمام کوئیزها برای ادمین", security: adminSecurity, responses: Object.fromEntries([ ["200", jsonResponse("لیست کوئیزها", { type: "array", items: { type: "object", additionalProperties: true } })], errorResponse("401", "نیازمند دسترسی ادمین", "Unauthorized"), ]), }, post: { tags: ["Admin", "Quiz"], summary: "ایجاد کوئیز جدید", security: adminSecurity, requestBody: requestBody( { $ref: "#/components/schemas/AdminQuizCreateRequest" }, { date: "2026-06-10", windowStart: "2026-06-10T08:00:00.000Z", windowEnd: "2026-06-10T20:00:00.000Z", goldWinnersCount: 1, silverWinnersCount: 2, bronzeWinnersCount: 0, questions: [ { questionText: "برنده بازی اول چه تیمی است؟", options: ["A", "B", "C", "D"], correctAnswer: 1, }, ], } ), responses: Object.fromEntries([ ["201", jsonResponse("کوئیز ایجاد شد", { type: "object", additionalProperties: true })], errorResponse("401", "نیازمند دسترسی ادمین", "Unauthorized"), ]), }, }, "/api/admin/quiz/{id}/lottery": { post: { tags: ["Admin", "Quiz", "Golden Cards"], summary: "اجرای قرعه‌کشی برندگان کوئیز", security: adminSecurity, parameters: [{ in: "path", name: "id", required: true, schema: { type: "string" } }], responses: Object.fromEntries([ ["200", jsonResponse("خروجی قرعه‌کشی", { type: "object", additionalProperties: true })], errorResponse("400", "خطای قرعه‌کشی", "قرعه‌کشی قبلاً انجام شده"), errorResponse("401", "نیازمند دسترسی ادمین", "Unauthorized"), errorResponse("404", "کوئیز پیدا نشد", "Quiz not found"), ]), }, }, "/api/golden-cards": { get: { tags: ["Golden Cards"], summary: "لیست کارت‌های طلایی کاربر جاری", security: authSecurity, responses: Object.fromEntries([ ["200", jsonResponse("لیست کارت‌ها", { type: "array", items: { $ref: "#/components/schemas/GoldenCard" } })], errorResponse("401", "نیازمند ورود", "Unauthorized"), ]), }, }, "/api/golden-cards/{id}/reveal": { post: { tags: ["Golden Cards"], summary: "باز کردن کارت طلایی", security: authSecurity, parameters: [{ in: "path", name: "id", required: true, schema: { type: "string" } }], responses: Object.fromEntries([ ["200", jsonResponse("کارت باز شد", { $ref: "#/components/schemas/GoldenCard" })], errorResponse("400", "کارت قبلاً باز شده", "کارت قبلاً باز شده"), errorResponse("401", "نیازمند ورود", "Unauthorized"), errorResponse("403", "کارت متعلق به کاربر نیست", "Forbidden"), errorResponse("404", "کارت پیدا نشد", "Card not found"), ]), }, }, "/api/admin/teams": { get: { tags: ["Admin", "Team"], summary: "لیست تیم‌ها برای ادمین", security: adminSecurity, responses: Object.fromEntries([ ["200", jsonResponse("لیست تیم‌ها", { type: "array", items: { type: "object", additionalProperties: true } })], errorResponse("401", "نیازمند دسترسی ادمین", "Unauthorized"), ]), }, }, "/api/admin/teams/{id}": { put: { tags: ["Admin", "Team"], summary: "تغییر وضعیت یک تیم", security: adminSecurity, parameters: [{ in: "path", name: "id", required: true, schema: { type: "string" } }], requestBody: requestBody( { $ref: "#/components/schemas/AdminTeamStatusUpdateRequest" }, { status: "APPROVED" } ), responses: Object.fromEntries([ ["200", jsonResponse("وضعیت تیم به‌روزرسانی شد", { $ref: "#/components/schemas/Team" })], errorResponse("401", "نیازمند دسترسی ادمین", "Unauthorized"), ]), }, }, "/api/admin/scoring": { put: { tags: ["Admin"], summary: "به‌روزرسانی قوانین امتیازدهی", security: adminSecurity, requestBody: requestBody( { type: "array", items: { $ref: "#/components/schemas/ScoringRuleInput" }, }, [{ position: "FWD", eventType: "GOAL", points: 4 }] ), responses: Object.fromEntries([ ["200", jsonResponse("قوانین ذخیره شدند", { $ref: "#/components/schemas/SuccessResponse" })], errorResponse("401", "نیازمند دسترسی ادمین", "Unauthorized"), ]), }, }, }, };