sendio/backend/app/api/imports.py
2026-01-13 05:17:57 +02:00

139 lines
4.7 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
from sqlalchemy.orm import Session
from typing import List
import pandas as pd
import io
from app.db.base import get_db
from app.core.deps import get_current_user
from app.models.user import User
from app.models.contact import Contact, DNDList
from app.schemas.imports import ImportSummary
from app.utils.phone import normalize_phone
router = APIRouter()
def process_import_file(
df: pd.DataFrame,
user_id: int,
source: str,
db: Session
) -> ImportSummary:
"""Process imported contacts dataframe"""
summary = ImportSummary(
total=len(df),
created=0,
updated=0,
skipped=0,
invalid=0,
errors=[]
)
# Normalize column names
df.columns = [col.lower().strip() for col in df.columns]
# Check required columns
if 'phone' not in df.columns:
summary.errors.append("Missing required column: phone")
summary.invalid = len(df)
return summary
for idx, row in df.iterrows():
try:
phone = str(row.get('phone', '')).strip()
if not phone:
summary.skipped += 1
continue
# Normalize phone
normalized_phone = normalize_phone(phone)
if not normalized_phone:
summary.invalid += 1
summary.errors.append(f"Row {idx+1}: Invalid phone number {phone}")
continue
# Check DND list
dnd_entry = db.query(DNDList).filter(
DNDList.user_id == user_id,
DNDList.phone_e164 == normalized_phone
).first()
if dnd_entry:
summary.skipped += 1
continue
# Check if exists
existing = db.query(Contact).filter(
Contact.user_id == user_id,
Contact.phone_e164 == normalized_phone
).first()
first_name = str(row.get('first_name', '')).strip() or None
last_name = str(row.get('last_name', '')).strip() or None
email = str(row.get('email', '')).strip() or None
opted_in_raw = str(row.get('opted_in', 'false')).lower()
opted_in = opted_in_raw in ['true', '1', 'yes', 'y']
if existing:
# Update existing
if first_name:
existing.first_name = first_name
if last_name:
existing.last_name = last_name
if email:
existing.email = email
existing.opted_in = opted_in
summary.updated += 1
else:
# Create new
contact = Contact(
user_id=user_id,
phone_e164=normalized_phone,
first_name=first_name,
last_name=last_name,
email=email,
opted_in=opted_in,
source=source
)
db.add(contact)
summary.created += 1
except Exception as e:
summary.invalid += 1
summary.errors.append(f"Row {idx+1}: {str(e)}")
db.commit()
return summary
@router.post("/excel", response_model=ImportSummary)
async def import_excel(
file: UploadFile = File(...),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
if not file.filename.endswith(('.xlsx', '.xls')):
raise HTTPException(status_code=400, detail="File must be Excel format (.xlsx or .xls)")
try:
contents = await file.read()
# Read phone column as string to preserve '+' sign
df = pd.read_excel(io.BytesIO(contents), dtype={'phone': str})
return process_import_file(df, current_user.id, "excel", db)
except Exception as e:
raise HTTPException(status_code=400, detail=f"Error processing file: {str(e)}")
@router.post("/csv", response_model=ImportSummary)
async def import_csv(
file: UploadFile = File(...),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
if not file.filename.endswith('.csv'):
raise HTTPException(status_code=400, detail="File must be CSV format")
try:
contents = await file.read()
# Read phone column as string to preserve '+' sign
df = pd.read_csv(io.StringIO(contents.decode('utf-8')), dtype={'phone': str})
return process_import_file(df, current_user.id, "csv", db)
except Exception as e:
raise HTTPException(status_code=400, detail=f"Error processing file: {str(e)}")