# app/api/v1/vendor/teams.py """ Vendor team member management endpoints. Implements complete team management with: - Team member listing - Invitation system - Role management - Permission checking - RBAC integration """ import logging from fastapi import APIRouter, Depends, Request from sqlalchemy.orm import Session from app.api.deps import ( get_current_vendor_api, get_user_permissions, require_vendor_owner, require_vendor_permission, ) from app.core.database import get_db from app.core.permissions import VendorPermissions from app.services.vendor_team_service import vendor_team_service from models.database.user import User from models.schema.team import ( BulkRemoveRequest, BulkRemoveResponse, InvitationAccept, InvitationAcceptResponse, InvitationResponse, RoleListResponse, TeamMemberInvite, TeamMemberListResponse, TeamMemberResponse, TeamMemberUpdate, TeamStatistics, UserPermissionsResponse, ) router = APIRouter(prefix="/team") logger = logging.getLogger(__name__) # ============================================================================ # Team Member Routes # ============================================================================ @router.get("/members", response_model=TeamMemberListResponse) def list_team_members( request: Request, include_inactive: bool = False, db: Session = Depends(get_db), current_user: User = Depends( require_vendor_permission(VendorPermissions.TEAM_VIEW.value) ), ): """ Get all team members for current vendor. **Required Permission:** `team.view` **Query Parameters:** - `include_inactive`: Include inactive team members (default: False) **Returns:** - List of team members with their roles and permissions - Statistics (total, active, pending) """ vendor = request.state.vendor members = vendor_team_service.get_team_members( db=db, vendor=vendor, include_inactive=include_inactive ) # Calculate statistics total = len(members) active = sum(1 for m in members if m["is_active"]) pending = sum(1 for m in members if m["invitation_pending"]) logger.info( f"Listed {total} team members for vendor {vendor.vendor_code} " f"(active: {active}, pending: {pending})" ) return TeamMemberListResponse( members=members, total=total, active_count=active, pending_invitations=pending ) @router.post("/invite", response_model=InvitationResponse) def invite_team_member( invitation: TeamMemberInvite, request: Request, db: Session = Depends(get_db), current_user: User = Depends(require_vendor_owner), # Owner only ): """ Invite a new team member to the vendor. **Required:** Vendor owner role **Process:** 1. Create user account (if doesn't exist) 2. Create VendorUser with invitation token 3. Send invitation email **Request Body:** - `email`: Email address of invitee - `first_name`, `last_name`: Optional names - `role_name`: Preset role (manager, staff, support, viewer, marketing) - `role_id`: Use existing role (alternative to role_name) - `custom_permissions`: Override role permissions (requires role_name) **Returns:** - Invitation details - Confirmation of email sent """ vendor = request.state.vendor # Determine role approach if invitation.role_id: # Use existing role by ID result = vendor_team_service.invite_team_member( db=db, vendor=vendor, inviter=current_user, email=invitation.email, role_id=invitation.role_id, ) elif invitation.role_name: # Use role name with optional custom permissions result = vendor_team_service.invite_team_member( db=db, vendor=vendor, inviter=current_user, email=invitation.email, role_name=invitation.role_name, custom_permissions=invitation.custom_permissions, ) else: # Default to Staff role result = vendor_team_service.invite_team_member( db=db, vendor=vendor, inviter=current_user, email=invitation.email, role_name="staff", ) db.commit() logger.info( f"Invitation sent: {invitation.email} to {vendor.vendor_code} " f"by {current_user.username}" ) return InvitationResponse( message="Invitation sent successfully", email=result["email"], role=result["role"], invitation_sent=True, ) @router.post("/accept-invitation", response_model=InvitationAcceptResponse) # public def accept_invitation(acceptance: InvitationAccept, db: Session = Depends(get_db)): """ Accept a team invitation and activate account. **No authentication required** - uses invitation token. **Request Body:** - `invitation_token`: Token from invitation email - `password`: New password (min 8 chars, must have upper, lower, digit) - `first_name`, `last_name`: User's name **Returns:** - Confirmation message - Vendor information - User information - Assigned role """ result = vendor_team_service.accept_invitation( db=db, invitation_token=acceptance.invitation_token, password=acceptance.password, first_name=acceptance.first_name, last_name=acceptance.last_name, ) db.commit() logger.info( f"Invitation accepted: {result['user'].email} " f"for vendor {result['vendor'].vendor_code}" ) return InvitationAcceptResponse( message="Invitation accepted successfully. You can now login.", vendor={ "id": result["vendor"].id, "vendor_code": result["vendor"].vendor_code, "name": result["vendor"].name, "subdomain": result["vendor"].subdomain, }, user={ "id": result["user"].id, "email": result["user"].email, "username": result["user"].username, "full_name": result["user"].full_name, }, role=result["role"], ) @router.get("/members/{user_id}", response_model=TeamMemberResponse) def get_team_member( user_id: int, request: Request, db: Session = Depends(get_db), current_user: User = Depends( require_vendor_permission(VendorPermissions.TEAM_VIEW.value) ), ): """ Get details of a specific team member. **Required Permission:** `team.view` """ vendor = request.state.vendor members = vendor_team_service.get_team_members( db=db, vendor=vendor, include_inactive=True ) member = next((m for m in members if m["id"] == user_id), None) if not member: from app.exceptions import UserNotFoundException raise UserNotFoundException(str(user_id)) return TeamMemberResponse(**member) @router.put("/members/{user_id}", response_model=TeamMemberResponse) def update_team_member( user_id: int, update_data: TeamMemberUpdate, request: Request, db: Session = Depends(get_db), current_user: User = Depends(require_vendor_owner), # Owner only ): """ Update a team member's role or status. **Required:** Vendor owner role **Cannot:** - Change owner's role - Remove owner **Request Body:** - `role_id`: New role ID (optional) - `is_active`: Active status (optional) """ vendor = request.state.vendor vendor_user = vendor_team_service.update_member_role( db=db, vendor=vendor, user_id=user_id, new_role_id=update_data.role_id, is_active=update_data.is_active, ) db.commit() logger.info( f"Team member updated: {user_id} in {vendor.vendor_code} " f"by {current_user.username}" ) # Return updated member details members = vendor_team_service.get_team_members(db, vendor, include_inactive=True) member = next((m for m in members if m["id"] == user_id), None) return TeamMemberResponse(**member) @router.delete("/members/{user_id}") def remove_team_member( user_id: int, request: Request, db: Session = Depends(get_db), current_user: User = Depends(require_vendor_owner), # Owner only ): """ Remove a team member from the vendor. **Required:** Vendor owner role **Cannot remove:** - Vendor owner **Action:** - Soft delete (sets is_active = False) - Member can be re-invited later """ vendor = request.state.vendor vendor_team_service.remove_team_member(db=db, vendor=vendor, user_id=user_id) db.commit() logger.info( f"Team member removed: {user_id} from {vendor.vendor_code} " f"by {current_user.username}" ) return {"message": "Team member removed successfully", "user_id": user_id} @router.post("/members/bulk-remove", response_model=BulkRemoveResponse) def bulk_remove_team_members( bulk_remove: BulkRemoveRequest, request: Request, db: Session = Depends(get_db), current_user: User = Depends(require_vendor_owner), ): """ Remove multiple team members at once. **Required:** Vendor owner role """ vendor = request.state.vendor success_count = 0 failed_count = 0 errors = [] for user_id in bulk_remove.user_ids: try: vendor_team_service.remove_team_member( db=db, vendor=vendor, user_id=user_id ) success_count += 1 except Exception as e: failed_count += 1 errors.append({"user_id": user_id, "error": str(e)}) db.commit() logger.info( f"Bulk remove completed: {success_count} removed, {failed_count} failed " f"in {vendor.vendor_code}" ) return BulkRemoveResponse( success_count=success_count, failed_count=failed_count, errors=errors ) # ============================================================================ # Role Management Routes # ============================================================================ @router.get("/roles", response_model=RoleListResponse) def list_roles( request: Request, db: Session = Depends(get_db), current_user: User = Depends( require_vendor_permission(VendorPermissions.TEAM_VIEW.value) ), ): """ Get all available roles for the vendor. **Required Permission:** `team.view` **Returns:** - List of roles with permissions - Includes both preset and custom roles """ vendor = request.state.vendor roles = vendor_team_service.get_vendor_roles(db=db, vendor_id=vendor.id) return RoleListResponse(roles=roles, total=len(roles)) # ============================================================================ # Permission Routes # ============================================================================ @router.get("/me/permissions", response_model=UserPermissionsResponse) def get_my_permissions( request: Request, permissions: list[str] = Depends(get_user_permissions), current_user: User = Depends(get_current_vendor_api), ): """ Get current user's permissions in this vendor. **Use this endpoint to:** - Determine what UI elements to show/hide - Check permissions before making API calls - Display user's role and capabilities **Returns:** - Complete list of permissions - Whether user is owner - Role name (if team member) Requires Authorization header (API endpoint). """ vendor = request.state.vendor is_owner = current_user.is_owner_of(vendor.id) role_name = current_user.get_vendor_role(vendor.id) return UserPermissionsResponse( permissions=permissions, permission_count=len(permissions), is_owner=is_owner, role_name=role_name, ) # ============================================================================ # Statistics Routes # ============================================================================ @router.get("/statistics", response_model=TeamStatistics) def get_team_statistics( request: Request, db: Session = Depends(get_db), current_user: User = Depends( require_vendor_permission(VendorPermissions.TEAM_VIEW.value) ), ): """ Get team statistics for the vendor. **Required Permission:** `team.view` **Returns:** - Total members - Active/inactive breakdown - Pending invitations - Owner count - Role distribution """ vendor = request.state.vendor members = vendor_team_service.get_team_members( db=db, vendor=vendor, include_inactive=True ) # Calculate statistics total = len(members) active = sum(1 for m in members if m["is_active"]) inactive = total - active pending = sum(1 for m in members if m["invitation_pending"]) owners = sum(1 for m in members if m["is_owner"]) team_members = total - owners # Role breakdown roles_breakdown = {} for member in members: role = member["role_name"] roles_breakdown[role] = roles_breakdown.get(role, 0) + 1 return TeamStatistics( total_members=total, active_members=active, inactive_members=inactive, pending_invitations=pending, owners=owners, team_members=team_members, roles_breakdown=roles_breakdown, )