swiftops-backend / src /app /api /v1 /bulk_invitations.py
kamau1's picture
implement CSV bulk invitations using in-memory analysis and metadata field
01f85fe
"""
Bulk Invitation Management Endpoints - CSV bulk user invitations
"""
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Form
from sqlalchemy.orm import Session
from typing import Optional
from uuid import UUID
from app.api.deps import get_db, get_current_active_user
from app.models.user import User
from app.schemas.bulk_invitation import (
BulkAnalysisResult,
BulkExecutionRequest,
BulkExecutionResult
)
from app.services.bulk_invitation_service import BulkInvitationService
from app.core.permissions import require_permission
import logging
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/invitations/bulk", tags=["Bulk Invitations"])
# Initialize service
bulk_invitation_service = BulkInvitationService()
@router.post("/analyze", response_model=BulkAnalysisResult, status_code=status.HTTP_201_CREATED)
@require_permission("invite_users")
async def analyze_bulk_invitation_csv(
csv_file: UploadFile = File(..., description="CSV file with user data"),
project_id: Optional[UUID] = Form(None, description="Project context (for project-level bulk invite)"),
client_id: Optional[UUID] = Form(None, description="Client context (for org-level bulk invite)"),
contractor_id: Optional[UUID] = Form(None, description="Contractor context (for org-level bulk invite)"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""
Upload and analyze CSV file for bulk invitations
**CSV Format:**
- Required columns: email, role
- Optional columns: first_name, last_name, phone, invitation_method
- Project-specific columns: project_role, region, subcontractor
**Context:**
- Provide ONE of: project_id, client_id, or contractor_id
- project_id: Bulk invite to specific project
- client_id/contractor_id: Bulk invite to organization
**Authorization:**
- platform_admin: Can bulk invite to any context
- project_manager: Can bulk invite to their projects
- dispatcher/sales_manager: Can bulk invite to their contractor's projects
- client_admin: Can bulk invite to their client
- contractor_admin: Can bulk invite to their contractor
**Returns:**
- Analysis of CSV with categorized users (in-memory, not persisted)
- Frontend should store this and send back selected users for execution
"""
# Validate file type
if not csv_file.filename.endswith('.csv'):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="File must be a CSV file"
)
# Validate context (one and only one)
contexts = [project_id, client_id, contractor_id]
non_none_count = sum(1 for ctx in contexts if ctx is not None)
if non_none_count == 0:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Must provide either project_id, client_id, or contractor_id"
)
if non_none_count > 1:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Can only provide one context: project_id, client_id, or contractor_id"
)
result = await bulk_invitation_service.analyze_csv(
csv_file=csv_file,
project_id=str(project_id) if project_id else None,
client_id=str(client_id) if client_id else None,
contractor_id=str(contractor_id) if contractor_id else None,
current_user=current_user,
db=db
)
return result
@router.post("/execute", response_model=BulkExecutionResult)
@require_permission("invite_users")
async def execute_bulk_invitation(
execution_request: BulkExecutionRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""
Execute bulk invitation import
**Process:**
1. Add existing users to project (if project context)
2. Send invitations to new users
3. Return detailed results
**Request Body:**
- users_to_add: List of existing users to add to project
- users_to_invite: List of new users to invite
- Context: project_id, client_id, or contractor_id
**Authorization:**
- Same as analyze endpoint
**Returns:**
- Detailed results with success/failure counts
- Individual results for each user
**Note:**
- Bulk operations are tracked in invitation_metadata.bulk_operation_id
- No session persistence - frontend manages the flow
"""
result = await bulk_invitation_service.execute_bulk_import(
execution_request=execution_request,
current_user=current_user,
db=db
)
return result
@router.get("/template/download")
async def download_csv_template(
template_type: str = "organization"
):
"""
Download CSV template for bulk invitations
**Template Types:**
- organization: For org-level bulk invites (simpler)
- project: For project-level bulk invites (includes project fields)
**Returns:**
- CSV file with headers and example row
"""
from fastapi.responses import StreamingResponse
import io
if template_type == "project":
headers = [
"email", "first_name", "last_name", "phone", "role",
"invitation_method", "project_role", "region", "subcontractor"
]
example_row = [
"john@example.com", "John", "Doe", "+254712345678", "field_agent",
"whatsapp", "Installer", "Nairobi West", ""
]
else:
headers = [
"email", "first_name", "last_name", "phone", "role", "invitation_method"
]
example_row = [
"john@example.com", "John", "Doe", "+254712345678", "field_agent", "whatsapp"
]
# Create CSV in memory
output = io.StringIO()
import csv
writer = csv.writer(output)
writer.writerow(headers)
writer.writerow(example_row)
# Return as downloadable file
output.seek(0)
return StreamingResponse(
iter([output.getvalue()]),
media_type="text/csv",
headers={
"Content-Disposition": f"attachment; filename=bulk_invitation_template_{template_type}.csv"
}
)