139 lines
4.7 KiB
Python
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)}")
|