Coverage for api/invites.py: 27.74%

216 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2026-04-15 12:31 +0000

1""" 

2Invite API endpoints for Missing Table 

3""" 

4 

5import os 

6import sys 

7from datetime import datetime 

8 

9import structlog 

10from fastapi import APIRouter, Depends, HTTPException, Query 

11from pydantic import BaseModel, Field 

12 

13sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 

14 

15logger = structlog.get_logger(__name__) 

16 

17from auth import get_current_user_required 

18from dao.match_dao import SupabaseConnection as DbConnectionHolder 

19from services import InviteService, TeamManagerService 

20from supabase import create_client 

21 

22# Initialize database connection with service role for admin operations 

23supabase_url = os.getenv("SUPABASE_URL", "") 

24service_key = os.getenv("SUPABASE_SERVICE_KEY") 

25 

26# Create service role client for admin operations that bypass RLS 

27if service_key: 27 ↛ 31line 27 didn't jump to line 31 because the condition on line 27 was always true

28 service_client = create_client(supabase_url, service_key) 

29else: 

30 # Fallback to regular connection if service key not available 

31 db_conn_holder_obj = DbConnectionHolder() 

32 service_client = db_conn_holder_obj.client 

33 

34router = APIRouter(prefix="/api/invites", tags=["invites"]) 

35 

36 

37# Pydantic models 

38class CreateInviteRequest(BaseModel): 

39 invite_type: str = Field(..., pattern="^(team_manager|team_player|team_fan)$") 

40 team_id: int 

41 age_group_id: int 

42 email: str | None = None 

43 player_id: int | None = None # Links to existing roster entry (for team_player invites) 

44 jersey_number: int | None = Field(None, ge=1, le=99) # Creates roster entry on redemption 

45 

46 

47class CreateClubManagerInviteRequest(BaseModel): 

48 club_id: int 

49 email: str | None = None 

50 

51 

52class ClubManagerInviteResponse(BaseModel): 

53 id: str 

54 invite_code: str 

55 invite_type: str 

56 club_id: int 

57 club_name: str | None 

58 email: str | None 

59 status: str 

60 expires_at: datetime 

61 created_at: datetime 

62 

63 

64class InviteCodeValidation(BaseModel): 

65 invite_code: str = Field(..., min_length=12, max_length=12) 

66 

67 

68class InviteResponse(BaseModel): 

69 id: str 

70 invite_code: str 

71 invite_type: str 

72 team_id: int 

73 team_name: str | None 

74 age_group_id: int 

75 age_group_name: str | None 

76 email: str | None 

77 status: str 

78 expires_at: datetime 

79 created_at: datetime 

80 

81 

82# Public endpoint - no auth required 

83@router.get("/validate/{invite_code}") 

84async def validate_invite_code(invite_code: str): 

85 """Validate an invite code without authentication""" 

86 # Use service client for validation to read any invite code 

87 invite_service = InviteService(service_client) 

88 

89 validation = invite_service.validate_invite_code(invite_code) 

90 

91 if not validation: 

92 raise HTTPException(status_code=404, detail="Invalid or expired invite code") 

93 

94 return validation 

95 

96 

97# Admin endpoints 

98@router.post("/admin/club-manager") 

99async def create_club_manager_invite( 

100 request: CreateClubManagerInviteRequest, current_user=Depends(get_current_user_required) 

101): 

102 """Create a club manager invitation (admin only)""" 

103 if current_user["role"] != "admin": 

104 raise HTTPException(status_code=403, detail="Only admins can create club manager invites") 

105 

106 # Use service role client for admin operations to bypass RLS 

107 invite_service = InviteService(service_client) 

108 

109 try: 

110 # Handle different user ID field names 

111 user_id = current_user.get("id") or current_user.get("user_id") or current_user.get("sub") 

112 if not user_id: 

113 raise HTTPException(status_code=400, detail=f"User ID not found in current_user: {current_user}") 

114 

115 invitation = invite_service.create_invitation( 

116 invited_by_user_id=user_id, 

117 invite_type="club_manager", 

118 club_id=request.club_id, 

119 email=request.email, 

120 ) 

121 

122 return invitation 

123 

124 except Exception as e: 

125 logger.exception("Club manager invite creation error") 

126 raise HTTPException(status_code=400, detail=str(e)) from e 

127 

128 

129@router.post("/admin/team-manager") 

130async def create_team_manager_invite(request: CreateInviteRequest, current_user=Depends(get_current_user_required)): 

131 """Create a team manager invitation (admin only)""" 

132 if current_user["role"] != "admin": 

133 raise HTTPException(status_code=403, detail="Only admins can create team manager invites") 

134 

135 # Use service role client for admin operations to bypass RLS 

136 invite_service = InviteService(service_client) 

137 

138 try: 

139 logger.debug( 

140 "Creating invite", 

141 current_user=current_user, 

142 team_id=request.team_id, 

143 age_group_id=request.age_group_id, 

144 email=request.email, 

145 ) 

146 

147 # Handle different user ID field names 

148 user_id = current_user.get("id") or current_user.get("user_id") or current_user.get("sub") 

149 if not user_id: 

150 raise HTTPException(status_code=400, detail=f"User ID not found in current_user: {current_user}") 

151 

152 invitation = invite_service.create_invitation( 

153 invited_by_user_id=user_id, 

154 invite_type="team_manager", 

155 team_id=request.team_id, 

156 age_group_id=request.age_group_id, 

157 email=request.email, 

158 ) 

159 

160 return invitation 

161 

162 except Exception as e: 

163 logger.exception("Invite creation error") 

164 raise HTTPException(status_code=400, detail=str(e)) from e 

165 

166 

167@router.post("/admin/club-fan") 

168async def create_club_fan_invite_admin( 

169 request: CreateClubManagerInviteRequest, current_user=Depends(get_current_user_required) 

170): 

171 """Create a club fan invitation (admin only)""" 

172 if current_user["role"] != "admin": 

173 raise HTTPException(status_code=403, detail="Only admins can create club fan invites") 

174 

175 # Use service role client for admin operations to bypass RLS 

176 invite_service = InviteService(service_client) 

177 

178 try: 

179 # Handle different user ID field names 

180 user_id = current_user.get("id") or current_user.get("user_id") or current_user.get("sub") 

181 if not user_id: 

182 raise HTTPException(status_code=400, detail=f"User ID not found in current_user: {current_user}") 

183 

184 invitation = invite_service.create_invitation( 

185 invited_by_user_id=user_id, 

186 invite_type="club_fan", 

187 club_id=request.club_id, 

188 email=request.email, 

189 ) 

190 

191 return invitation 

192 

193 except Exception as e: 

194 logger.exception("Club fan invite creation error") 

195 raise HTTPException(status_code=400, detail=str(e)) from e 

196 

197 

198@router.post("/admin/team-fan") 

199async def create_team_fan_invite_admin(request: CreateInviteRequest, current_user=Depends(get_current_user_required)): 

200 """Create a team fan invitation (admin) - DEPRECATED: Use club-fan instead""" 

201 if current_user["role"] != "admin": 

202 raise HTTPException(status_code=403, detail="Unauthorized") 

203 

204 # Use service role client for admin operations to bypass RLS 

205 invite_service = InviteService(service_client) 

206 

207 try: 

208 # Handle different user ID field names 

209 user_id = current_user.get("id") or current_user.get("user_id") or current_user.get("sub") 

210 if not user_id: 

211 raise HTTPException(status_code=400, detail=f"User ID not found in current_user: {current_user}") 

212 

213 invitation = invite_service.create_invitation( 

214 invited_by_user_id=user_id, 

215 invite_type="team_fan", 

216 team_id=request.team_id, 

217 age_group_id=request.age_group_id, 

218 email=request.email, 

219 ) 

220 

221 return invitation 

222 

223 except Exception as e: 

224 raise HTTPException(status_code=400, detail=str(e)) from e 

225 

226 

227@router.post( 

228 "/admin/team-player", 

229) 

230async def create_team_player_invite_admin( 

231 request: CreateInviteRequest, current_user=Depends(get_current_user_required) 

232): 

233 """Create a team player invitation (admin). 

234 

235 If player_id is provided, the invitation will be linked to an existing roster entry. 

236 If jersey_number is provided (without player_id), a roster entry will be created 

237 when the player accepts the invite. 

238 """ 

239 if current_user["role"] != "admin": 

240 raise HTTPException(status_code=403, detail="Unauthorized") 

241 

242 # Use service role client for admin operations to bypass RLS 

243 invite_service = InviteService(service_client) 

244 

245 try: 

246 # Handle different user ID field names 

247 user_id = current_user.get("id") or current_user.get("user_id") or current_user.get("sub") 

248 if not user_id: 

249 raise HTTPException(status_code=400, detail=f"User ID not found in current_user: {current_user}") 

250 

251 invitation = invite_service.create_invitation( 

252 invited_by_user_id=user_id, 

253 invite_type="team_player", 

254 team_id=request.team_id, 

255 age_group_id=request.age_group_id, 

256 email=request.email, 

257 player_id=request.player_id, 

258 jersey_number=request.jersey_number, 

259 ) 

260 

261 return invitation 

262 

263 except Exception as e: 

264 raise HTTPException(status_code=400, detail=str(e)) from e 

265 

266 

267# Club manager endpoints 

268@router.post("/club-manager/club-fan") 

269async def create_club_fan_invite_club_manager( 

270 request: CreateClubManagerInviteRequest, current_user=Depends(get_current_user_required) 

271): 

272 """Create a club fan invitation (club manager or admin)""" 

273 if current_user["role"] not in ["admin", "club_manager"]: 

274 raise HTTPException(status_code=403, detail="Only club managers or admins can create club fan invites") 

275 

276 # Use service role client for operations to bypass RLS 

277 invite_service = InviteService(service_client) 

278 

279 try: 

280 # Handle different user ID field names 

281 user_id = current_user.get("id") or current_user.get("user_id") or current_user.get("sub") 

282 if not user_id: 

283 raise HTTPException(status_code=400, detail=f"User ID not found in current_user: {current_user}") 

284 

285 # Club managers can only create invites for their own club 

286 if current_user["role"] == "club_manager": 

287 user_club_id = current_user.get("club_id") 

288 if not user_club_id or user_club_id != request.club_id: 

289 raise HTTPException(status_code=403, detail="You can only create fan invites for your own club") 

290 

291 invitation = invite_service.create_invitation( 

292 invited_by_user_id=user_id, 

293 invite_type="club_fan", 

294 club_id=request.club_id, 

295 email=request.email, 

296 ) 

297 

298 return invitation 

299 

300 except Exception as e: 

301 logger.exception("Club fan invite creation error") 

302 raise HTTPException(status_code=400, detail=str(e)) from e 

303 

304 

305# Team manager endpoints 

306@router.post("/team-manager/team-fan") 

307async def create_team_fan_invite(request: CreateInviteRequest, current_user=Depends(get_current_user_required)): 

308 """Create a team fan invitation (team manager) - DEPRECATED: Use club-fan instead""" 

309 if current_user["role"] not in ["admin", "team-manager", "team_manager"]: 

310 raise HTTPException(status_code=403, detail="Unauthorized") 

311 

312 supabase = service_client 

313 team_manager_service = TeamManagerService(supabase) 

314 

315 # Handle different user ID field names 

316 user_id = current_user.get("id") or current_user.get("user_id") or current_user.get("sub") 

317 if not user_id: 

318 raise HTTPException(status_code=400, detail=f"User ID not found in current_user: {current_user}") 

319 

320 # Check if team manager can manage this team 

321 if current_user["role"] in ["team_manager", "team-manager"]: 

322 can_manage = team_manager_service.can_manage_team(user_id, request.team_id, request.age_group_id) 

323 

324 if not can_manage: 

325 raise HTTPException(status_code=403, detail="You can only create invites for teams you manage") 

326 

327 invite_service = InviteService(supabase) 

328 

329 try: 

330 invitation = invite_service.create_invitation( 

331 invited_by_user_id=user_id, 

332 invite_type="team_fan", 

333 team_id=request.team_id, 

334 age_group_id=request.age_group_id, 

335 email=request.email, 

336 ) 

337 

338 return invitation 

339 

340 except Exception as e: 

341 raise HTTPException(status_code=400, detail=str(e)) from e 

342 

343 

344@router.post( 

345 "/team-manager/team-player", 

346) 

347async def create_team_player_invite(request: CreateInviteRequest, current_user=Depends(get_current_user_required)): 

348 """Create a team player invitation (team manager). 

349 

350 If player_id is provided, the invitation will be linked to an existing roster entry. 

351 If jersey_number is provided (without player_id), a roster entry will be created 

352 when the player accepts the invite. 

353 """ 

354 if current_user["role"] not in ["admin", "team-manager", "team_manager"]: 

355 raise HTTPException(status_code=403, detail="Unauthorized") 

356 

357 supabase = service_client 

358 team_manager_service = TeamManagerService(supabase) 

359 

360 # Handle different user ID field names 

361 user_id = current_user.get("id") or current_user.get("user_id") or current_user.get("sub") 

362 if not user_id: 

363 raise HTTPException(status_code=400, detail=f"User ID not found in current_user: {current_user}") 

364 

365 # Check if team manager can manage this team 

366 if current_user["role"] in ["team_manager", "team-manager"]: 

367 can_manage = team_manager_service.can_manage_team(user_id, request.team_id, request.age_group_id) 

368 

369 if not can_manage: 

370 raise HTTPException(status_code=403, detail="You can only create invites for teams you manage") 

371 

372 invite_service = InviteService(supabase) 

373 

374 try: 

375 invitation = invite_service.create_invitation( 

376 invited_by_user_id=user_id, 

377 invite_type="team_player", 

378 team_id=request.team_id, 

379 age_group_id=request.age_group_id, 

380 email=request.email, 

381 player_id=request.player_id, 

382 jersey_number=request.jersey_number, 

383 ) 

384 

385 return invitation 

386 

387 except Exception as e: 

388 raise HTTPException(status_code=400, detail=str(e)) from e 

389 

390 

391# List user's invitations 

392@router.get( 

393 "/my-invites", 

394) 

395async def get_my_invitations( 

396 current_user=Depends(get_current_user_required), 

397 status: str | None = Query(None, pattern="^(pending|used|expired)$"), 

398): 

399 """Get all invitations created by the current user""" 

400 supabase = service_client 

401 invite_service = InviteService(supabase) 

402 

403 # Handle different user ID field names 

404 user_id = current_user.get("id") or current_user.get("user_id") or current_user.get("sub") 

405 if not user_id: 

406 raise HTTPException(status_code=400, detail=f"User ID not found in current_user: {current_user}") 

407 

408 invitations = invite_service.get_user_invitations(user_id) 

409 

410 # Filter by status if provided 

411 if status: 

412 invitations = [inv for inv in invitations if inv["status"] == status] 

413 

414 return invitations 

415 

416 

417# Cancel invitation 

418@router.delete( 

419 "/{invite_id}", 

420) 

421async def cancel_invitation(invite_id: str, current_user=Depends(get_current_user_required)): 

422 """Cancel a pending invitation""" 

423 supabase = service_client 

424 invite_service = InviteService(supabase) 

425 

426 # Check if user owns this invitation or is admin 

427 invitations = invite_service.get_user_invitations(current_user["user_id"]) 

428 user_owns_invite = any(inv["id"] == invite_id for inv in invitations) 

429 

430 if not user_owns_invite and current_user["role"] != "admin": 

431 raise HTTPException(status_code=403, detail="You can only cancel your own invitations") 

432 

433 success = invite_service.cancel_invitation(invite_id, current_user["user_id"]) 

434 

435 if not success: 

436 raise HTTPException(status_code=404, detail="Invitation not found or already cancelled") 

437 

438 return {"message": "Invitation cancelled successfully"} 

439 

440 

441# Team manager assignments endpoint 

442@router.get( 

443 "/team-manager/assignments", 

444) 

445async def get_team_manager_assignments(current_user=Depends(get_current_user_required)): 

446 """Get team assignments for the current user""" 

447 if current_user["role"] not in ["admin", "team-manager", "team_manager"]: 

448 raise HTTPException(status_code=403, detail="Unauthorized") 

449 

450 supabase = service_client 

451 team_manager_service = TeamManagerService(supabase) 

452 

453 assignments = team_manager_service.get_user_team_assignments(current_user["id"]) 

454 

455 return assignments