import secrets 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) token = secrets.token_urlsafe(32) 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=0, active=1, user_id=db_customer.id, confirmation_token=token, confirmation_sent_at=datetime.utcnow() ) db.add(db_user) await db.commit() await db.refresh(db_user) # In a real application, you would send an email here # For now, we'll just print the confirmation URL to the console confirmation_url = f"http://localhost:3000/confirm-email?token={token}" print(f"Confirmation URL: {confirmation_url}") 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': 0, # 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) token = secrets.token_urlsafe(32) 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=0, active=1, user_id=customer.id, confirmation_token=token, confirmation_sent_at=datetime.utcnow() ) db.add(db_user) await db.commit() await db.refresh(db_user) # In a real application, you would send an email here # For now, we'll just print the confirmation URL to the console confirmation_url = f"http://localhost:3000/confirm-email?token={token}" print(f"Confirmation URL: {confirmation_url}") return db_user