import random import string import os import shutil from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from database import get_db from models import Customer_Customer, Customer_Description, Customer_Tank_Inspection, Customer_Stats, Account_User from schemas import NewCustomerCreate, UserResponse, CustomerCreateStep1, CustomerAccountCreate from passlib.context import CryptContext from datetime import datetime from PIL import Image import io pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto") def generate_random_number_string(length): if length < 1: raise ValueError("Length must be at least 1") random_number = ''.join(random.choices(string.digits, k=length)) return random_number def get_password_hash(password): # Truncate password to 72 bytes max for bcrypt compatibility truncated_password = password.encode('utf-8')[:72].decode('utf-8', errors='ignore') return pwd_context.hash(truncated_password) def resize_image(image_data, max_size=(1024, 1024), quality=85): """ Resize image to fit within max_size while maintaining aspect ratio. Convert to JPEG format and strip metadata for security. """ try: # Open image from bytes img = Image.open(io.BytesIO(image_data)) # Convert to RGB if necessary (for PNG with transparency, etc.) if img.mode in ('RGBA', 'LA', 'P'): img = img.convert('RGB') # Resize if larger than max_size if img.width > max_size[0] or img.height > max_size[1]: img.thumbnail(max_size, Image.Resampling.LANCZOS) # Save as JPEG with specified quality output = io.BytesIO() img.save(output, format='JPEG', quality=quality, optimize=True) output.seek(0) return output.getvalue() except Exception as e: raise HTTPException(status_code=400, detail=f"Invalid image file: {str(e)}") router = APIRouter() @router.post("/new", response_model=UserResponse) async def register_new_customer(customer: NewCustomerCreate, db: AsyncSession = Depends(get_db)): # Verify passwords match if customer.password != customer.confirm_password: raise HTTPException(status_code=400, detail="Passwords do not match") # Check if email already registered result = await db.execute(select(Account_User).where(Account_User.email == customer.customer_email)) if result.scalar_one_or_none(): raise HTTPException(status_code=400, detail="Email already registered") while True: random_part = generate_random_number_string(6) account_number = 'AO-' + random_part result = await db.execute(select(Customer_Customer).where(Customer_Customer.account_number == account_number)) existing_customer = result.scalar_one_or_none() if not existing_customer: break # Create customer customer_data = customer.model_dump() customer_data.pop('password') customer_data.pop('confirm_password') customer_data.update({ 'account_number': account_number, 'customer_state': 1, # Default 'customer_automatic': 0, 'company_id': 1, # Default 'customer_latitude': '0', 'customer_longitude': '0', 'correct_address': True, 'customer_first_call': datetime.utcnow() }) db_customer = Customer_Customer(**customer_data) db.add(db_customer) await db.commit() await db.refresh(db_customer) # Extract house number from customer address (first part before space) house_number = customer.customer_address.split()[0] if customer.customer_address else '' # Create account user username = account_number hashed_password = get_password_hash(customer.password) db_user = Account_User( username=username, account_number=account_number, house_number=house_number, password_hash=hashed_password, member_since=datetime.utcnow(), email=customer.customer_email, last_seen=datetime.utcnow(), admin=0, admin_role=0, confirmed=1, active=1, user_id=db_customer.id ) db.add(db_user) await db.commit() await db.refresh(db_user) return db_user @router.post("/step3") async def upload_tank_images( account_number: str = Form(...), tank_image_1: UploadFile = File(...), tank_image_2: UploadFile = File(...), tank_image_3: UploadFile = File(...), db: AsyncSession = Depends(get_db) ): print("=== STEP3 DEBUG START ===") print(f"Account number received: '{account_number}'") # Debug: Check all parameters received images = [tank_image_1, tank_image_2, tank_image_3] for i, image in enumerate(images, 1): print(f"Image {i}: filename='{image.filename}', content_type='{image.content_type}', size={image.size}") # Validate account number if not account_number: print("ERROR: Account number is empty") raise HTTPException(status_code=400, detail="Account number is required") # Get customer info for description record customer_result = await db.execute(select(Customer_Customer).where(Customer_Customer.account_number == account_number)) customer = customer_result.scalar_one_or_none() if not customer: raise HTTPException(status_code=400, detail="Customer not found") print(f"Creating directory: /images/{account_number}") # Create directory for account number in the mounted images volume account_dir = f"/images/{account_number}" os.makedirs(account_dir, exist_ok=True) current_datetime = datetime.utcnow().strftime("%Y-%m-%d_%H-%M-%S") # YYYY-MM-DD_HH-MM-SS format # Track if images were uploaded successfully images_uploaded_successfully = False # Validate and save images saved_files = [] try: for i, image in enumerate(images, 1): print(f"Processing image {i}...") # Read image data image_data = await image.read() print(f"Image {i} data read: {len(image_data)} bytes") # Validate file size (max 20MB before processing) if len(image_data) > 20 * 1024 * 1024: print(f"ERROR: Image {i} too large: {len(image_data)} bytes") raise HTTPException(status_code=400, detail=f"File {i} is too large (max 20MB)") # Resize and process image (this also validates it's a valid image) print(f"Resizing image {i}...") processed_image_data = resize_image(image_data, max_size=(1024, 1024), quality=85) print(f"Image {i} resized: {len(processed_image_data)} bytes") # Save processed image with datetime-based filename filename = f"{current_datetime}-{i}.jpg" file_path = os.path.join(account_dir, filename) print(f"Saving image {i} to: {file_path}") with open(file_path, "wb") as buffer: buffer.write(processed_image_data) saved_files.append(filename) print(f"Image {i} saved successfully") images_uploaded_successfully = True print(f"All images processed successfully. Saved files: {saved_files}") except Exception as e: print(f"ERROR processing images: {str(e)}") # Don't raise exception - we want to track the failure in the database images_uploaded_successfully = False # Update or create customer tank inspection record with tank_images status print("Updating customer tank inspection record...") tank_result = await db.execute( select(Customer_Tank_Inspection).where(Customer_Tank_Inspection.customer_id == customer.id) ) existing_tank = tank_result.scalar_one_or_none() if existing_tank: # Update existing record if images_uploaded_successfully: existing_tank.tank_images += 1 # Increment count of image sets # Append current datetime to upload dates list current_dates = existing_tank.tank_image_upload_dates or [] current_dates.append(current_datetime) existing_tank.tank_image_upload_dates = current_dates await db.commit() print(f"Updated existing tank inspection record with tank_images = {existing_tank.tank_images}, dates = {existing_tank.tank_image_upload_dates}") else: # Create new record new_tank = Customer_Tank_Inspection( customer_id=customer.id, tank_images=1 if images_uploaded_successfully else 0, tank_image_upload_dates=[current_datetime] if images_uploaded_successfully else [] # Other fields will be null/None initially ) db.add(new_tank) await db.commit() print(f"Created new tank inspection record with tank_images = {new_tank.tank_images}, dates = {new_tank.tank_image_upload_dates}") print("=== STEP3 DEBUG END ===") if images_uploaded_successfully: return { "message": "Tank images uploaded successfully", "account_number": account_number, "uploaded_files": saved_files } else: return { "message": "Tank images upload skipped or failed", "account_number": account_number, "uploaded_files": [] } @router.post("/upload-tank-images") async def upload_additional_tank_images( account_number: str = Form(...), tank_image_1: UploadFile = File(...), tank_image_2: UploadFile = File(...), tank_image_3: UploadFile = File(...), db: AsyncSession = Depends(get_db) ): """ Endpoint for customers to upload additional sets of 3 tank images after registration. Similar to step3 but for existing customers. """ print("=== UPLOAD ADDITIONAL TANK IMAGES DEBUG START ===") print(f"Account number received: '{account_number}'") # Debug: Check all parameters received images = [tank_image_1, tank_image_2, tank_image_3] for i, image in enumerate(images, 1): print(f"Image {i}: filename='{image.filename}', content_type='{image.content_type}', size={image.size}") # Validate account number if not account_number: print("ERROR: Account number is empty") raise HTTPException(status_code=400, detail="Account number is required") # Get customer info customer_result = await db.execute(select(Customer_Customer).where(Customer_Customer.account_number == account_number)) customer = customer_result.scalar_one_or_none() if not customer: raise HTTPException(status_code=400, detail="Customer not found") print(f"Creating directory: /images/{account_number}") # Create directory for account number in the mounted images volume account_dir = f"/images/{account_number}" os.makedirs(account_dir, exist_ok=True) current_datetime = datetime.utcnow().strftime("%Y-%m-%d_%H-%M-%S") # YYYY-MM-DD_HH-MM-SS format # Track if images were uploaded successfully images_uploaded_successfully = False # Validate and save images saved_files = [] try: for i, image in enumerate(images, 1): print(f"Processing image {i}...") # Read image data image_data = await image.read() print(f"Image {i} data read: {len(image_data)} bytes") # Validate file size (max 20MB before processing) if len(image_data) > 20 * 1024 * 1024: print(f"ERROR: Image {i} too large: {len(image_data)} bytes") raise HTTPException(status_code=400, detail=f"File {i} is too large (max 20MB)") # Resize and process image (this also validates it's a valid image) print(f"Resizing image {i}...") processed_image_data = resize_image(image_data, max_size=(1024, 1024), quality=85) print(f"Image {i} resized: {len(processed_image_data)} bytes") # Save processed image with datetime-based filename filename = f"{current_datetime}-{i}.jpg" file_path = os.path.join(account_dir, filename) print(f"Saving image {i} to: {file_path}") with open(file_path, "wb") as buffer: buffer.write(processed_image_data) saved_files.append(filename) print(f"Image {i} saved successfully") images_uploaded_successfully = True print(f"All images processed successfully. Saved files: {saved_files}") except Exception as e: print(f"ERROR processing images: {str(e)}") # Don't raise exception - we want to track the failure in the database images_uploaded_successfully = False # Update customer tank inspection record print("Updating customer tank inspection record...") tank_result = await db.execute( select(Customer_Tank_Inspection).where(Customer_Tank_Inspection.customer_id == customer.id) ) existing_tank = tank_result.scalar_one_or_none() if existing_tank: # Update existing record if images_uploaded_successfully: existing_tank.tank_images += 1 # Increment count of image sets # Append current datetime to upload dates list current_dates = existing_tank.tank_image_upload_dates or [] current_dates.append(current_datetime) existing_tank.tank_image_upload_dates = current_dates await db.commit() print(f"Updated existing tank inspection record with tank_images = {existing_tank.tank_images}, dates = {existing_tank.tank_image_upload_dates}") else: # This shouldn't happen for additional uploads, but handle it just in case new_tank = Customer_Tank_Inspection( customer_id=customer.id, tank_images=1 if images_uploaded_successfully else 0, tank_image_upload_dates=[current_datetime] if images_uploaded_successfully else [] ) db.add(new_tank) await db.commit() print(f"Created new tank inspection record with tank_images = {new_tank.tank_images}, dates = {new_tank.tank_image_upload_dates}") print("=== UPLOAD ADDITIONAL TANK IMAGES DEBUG END ===") if images_uploaded_successfully: return { "message": "Additional tank images uploaded successfully", "account_number": account_number, "uploaded_files": saved_files, "upload_date": current_datetime } else: raise HTTPException(status_code=400, detail="Failed to upload tank images") @router.get("/tank-images/{account_number}") async def get_tank_images(account_number: str, db: AsyncSession = Depends(get_db)): """ Get tank images information for a customer including upload dates. """ # Get customer info customer_result = await db.execute(select(Customer_Customer).where(Customer_Customer.account_number == account_number)) customer = customer_result.scalar_one_or_none() if not customer: raise HTTPException(status_code=404, detail="Customer not found") # Get tank inspection record tank_result = await db.execute( select(Customer_Tank_Inspection).where(Customer_Tank_Inspection.customer_id == customer.id) ) tank_record = tank_result.scalar_one_or_none() # Build image sets - first from database, then scan for any additional files image_sets = [] upload_dates = tank_record.tank_image_upload_dates or [] if tank_record else [] # Add sets from database for i, upload_date in enumerate(upload_dates): # Handle backward compatibility: old format used date-only strings and tank_*.jpg files # New format uses datetime strings and {datetime}-*.jpg files if "_" in upload_date: # New datetime format (YYYY-MM-DD_HH-MM-SS) image_set = { "date": upload_date, "images": [ f"/images/{account_number}/{upload_date}-1.jpg", f"/images/{account_number}/{upload_date}-2.jpg", f"/images/{account_number}/{upload_date}-3.jpg" ] } else: # Old date-only format (YYYY-MM-DD) - uses tank_*.jpg files image_set = { "date": upload_date, "images": [ f"/images/{account_number}/tank_1.jpg", f"/images/{account_number}/tank_2.jpg", f"/images/{account_number}/tank_3.jpg" ] } image_sets.append(image_set) # Scan for any additional image sets that might not be in database account_dir = f"/images/{account_number}" if os.path.exists(account_dir): # Find all image files all_files = os.listdir(account_dir) image_files = [f for f in all_files if f.endswith('.jpg')] # Group by datetime prefix datetime_groups = {} for file in image_files: if file.startswith('tank_'): # Old tank_*.jpg files - already handled above continue elif '_' in file and file.endswith('.jpg'): # datetime format like 2026-01-08_23-25-31-1.jpg # Extract datetime prefix by removing the image number and .jpg # Example: 2026-01-08_23-25-31-1.jpg -> 2026-01-08_23-25-31 datetime_prefix = file.rsplit('-', 1)[0] # Remove everything after last dash if datetime_prefix not in datetime_groups: datetime_groups[datetime_prefix] = [] datetime_groups[datetime_prefix].append(file) # Add any datetime groups not already in database for datetime_prefix, files in datetime_groups.items(): if datetime_prefix not in upload_dates and len(files) >= 3: # Sort files to ensure correct order sorted_files = sorted(files) image_set = { "date": datetime_prefix, "images": [ f"/images/{account_number}/{sorted_files[0]}", f"/images/{account_number}/{sorted_files[1]}", f"/images/{account_number}/{sorted_files[2]}" ] } image_sets.append(image_set) # Also check for date-prefixed files (like 2026-01-08-1.jpg) date_groups = {} for file in image_files: if file.startswith('tank_') or '_' in file: continue # Skip old format and datetime format parts = file.split('-') if len(parts) == 4 and parts[3] in ['1.jpg', '2.jpg', '3.jpg']: date_prefix = '-'.join(parts[:3]) # 2026-01-08 if date_prefix not in date_groups: date_groups[date_prefix] = [] date_groups[date_prefix].append(file) # Add date groups not already in database for date_prefix, files in date_groups.items(): if date_prefix not in upload_dates and len(files) >= 3: # Sort files to ensure correct order sorted_files = sorted(files) image_set = { "date": date_prefix, "images": [ f"/images/{account_number}/{sorted_files[0]}", f"/images/{account_number}/{sorted_files[1]}", f"/images/{account_number}/{sorted_files[2]}" ] } image_sets.append(image_set) # Sort image sets by date descending (newest first) def sort_key(item): date_str = item['date'] try: if '_' in date_str: return datetime.strptime(date_str, "%Y-%m-%d_%H-%M-%S") else: return datetime.strptime(date_str, "%Y-%m-%d") except: return datetime.min image_sets.sort(key=sort_key, reverse=True) return { "account_number": account_number, "image_sets": image_sets } @router.post("/step1") async def create_customer_step1(customer: CustomerCreateStep1, db: AsyncSession = Depends(get_db)): while True: random_part = generate_random_number_string(6) account_number = 'AO-' + random_part result = await db.execute(select(Customer_Customer).where(Customer_Customer.account_number == account_number)) existing_customer = result.scalar_one_or_none() if not existing_customer: break # Extract house_description for separate table house_description = customer.house_description # Create customer customer_data = customer.model_dump() customer_data.pop('house_description') # Remove from customer data customer_data.update({ 'account_number': account_number, 'customer_state': 1, # Default 'customer_automatic': 0, 'company_id': 1, # Default 'customer_latitude': '0', 'customer_longitude': '0', 'correct_address': True, 'customer_first_call': datetime.utcnow() }) db_customer = Customer_Customer(**customer_data) db.add(db_customer) await db.commit() await db.refresh(db_customer) # Create customer description if house_description provided if house_description: db_description = Customer_Description( customer_id=db_customer.id, account_number=account_number, company_id=1, # Default fill_location=None, # Will work on this later description=house_description # tank_images is now tracked in customer_tank table ) db.add(db_description) await db.commit() # Create customer stats record for tracking metrics db_stats = Customer_Stats( customer_id=db_customer.id # All other fields default to 0/0.00 as defined in the model ) db.add(db_stats) await db.commit() return {"account_number": account_number} @router.post("/step2", response_model=UserResponse) async def create_customer_account(account_data: CustomerAccountCreate, db: AsyncSession = Depends(get_db)): # Verify passwords match if account_data.password != account_data.confirm_password: raise HTTPException(status_code=400, detail="Passwords do not match") # Check if customer exists result = await db.execute(select(Customer_Customer).where(Customer_Customer.account_number == account_data.account_number)) customer = result.scalar_one_or_none() if not customer: raise HTTPException(status_code=400, detail="Customer not found") # Check if email already registered result = await db.execute(select(Account_User).where(Account_User.email == account_data.customer_email)) if result.scalar_one_or_none(): raise HTTPException(status_code=400, detail="Email already registered") # Update customer email customer.customer_email = account_data.customer_email db.add(customer) await db.commit() # Extract house number from customer address (first part before space) house_number = customer.customer_address.split()[0] if customer.customer_address else '' # Create account user username = account_data.account_number hashed_password = get_password_hash(account_data.password) db_user = Account_User( username=username, account_number=account_data.account_number, house_number=house_number, password_hash=hashed_password, member_since=datetime.utcnow(), email=account_data.customer_email, last_seen=datetime.utcnow(), admin=0, admin_role=0, confirmed=1, active=1, user_id=customer.id ) db.add(db_user) await db.commit() await db.refresh(db_user) return db_user