from flask import Blueprint, render_template, redirect, url_for, request, jsonify, current_app, flash from flask_login import current_user, login_required, login_user, logout_user from app.models import Quotation, User from app import db from app.forms import LoginForm, QuotationForm from app.utils.cost_factor_loader import get_cost_factors from app.config import ALLOWED_EMAILS import logging from datetime import datetime, timezone, timedelta from app import limiter, cache import json from ..utils.calculator import calculate_base_price from ..health import get_health_status bp = Blueprint('main', __name__) logger = logging.getLogger(__name__) @bp.route('/', methods=['GET', 'POST']) def home(): if current_user.is_authenticated: return redirect(url_for('main.calculator')) return redirect(url_for('auth.login')) @bp.route('/calculator') @login_required def calculator(): try: form = QuotationForm() # Set today's date as default for issue_date if not form.issue_date.data: form.issue_date.data = datetime.now(timezone.utc).date() # Get cost factors for the form cost_factors, tax_rates, default_net_base = get_cost_factors() # Initialize form with default values if form.mold_main_type.data in form.MOLD_TYPE_MAPPING: form.mold_sub_type.choices = [('', 'Select Sub Type')] + [(x, x) for x in form.MOLD_TYPE_MAPPING[form.mold_main_type.data]] # Get quote history with error handling try: quote_history = Quotation.query.filter_by(user_id=current_user.id)\ .order_by(Quotation.created_at.desc())\ .limit(5)\ .all() except Exception as e: current_app.logger.error(f"Error fetching quote history: {str(e)}") quote_history = [] return render_template('calculator.html', form=form, quote_history=quote_history, tax_rates=tax_rates, quotation=None) except Exception as e: current_app.logger.error(f"Calculator error: {str(e)}", exc_info=True) flash('Error loading calculator data', 'danger') # Create a new form with default values form = QuotationForm() if form.mold_main_type.data in form.MOLD_TYPE_MAPPING: form.mold_sub_type.choices = [('', 'Select Sub Type')] + [(x, x) for x in form.MOLD_TYPE_MAPPING[form.mold_main_type.data]] # Set today's date as default even in error case form.issue_date.data = datetime.now(timezone.utc).date() return render_template('calculator.html', form=form, quote_history=[], tax_rates={}, quotation=None) @bp.route('/calculate', methods=['GET', 'POST']) @login_required @limiter.limit("30 per minute") @cache.cached(timeout=300, key_prefix='calculate_form') def calculate(): """Calculate mold cost based on form data""" try: form = QuotationForm() # Log incoming request data current_app.logger.info('Incoming request data:') current_app.logger.info(f'Request method: {request.method}') current_app.logger.info(f'Request form data: {dict(request.form)}') current_app.logger.info(f'Request JSON data: {request.get_json(silent=True)}') if request.method == 'POST': try: # Log form data before validation current_app.logger.info('Form data before validation:') current_app.logger.info(f'Form data: {form.data}') current_app.logger.info(f'Form errors: {form.errors}') # Log the selected mold type current_app.logger.info(f'Selected mold_main_type: {form.mold_main_type.data}') current_app.logger.info(f'Selected mold_sub_type: {form.mold_sub_type.data}') current_app.logger.info(f'Selected mold_specific_type: {form.mold_specific_type.data}') # Update mold_sub_type choices based on selected mold_main_type main_type = request.form.get('mold_main_type') current_app.logger.info(f'Updating sub_type choices for main_type: {main_type}') if main_type in form.MOLD_TYPE_MAPPING: form.mold_sub_type.choices = [('', 'Select Sub Type')] + [(x, x) for x in form.MOLD_TYPE_MAPPING[main_type]] current_app.logger.info(f'Updated sub_type choices: {form.mold_sub_type.choices}') # Update mold_specific_type choices based on selected mold_sub_type sub_type = request.form.get('mold_sub_type') if sub_type in form.MOLD_SPECIFIC_TYPE_OPTIONS: form.mold_specific_type.choices = [('', 'Select Specific Type')] + [(x, x) for x in form.MOLD_SPECIFIC_TYPE_OPTIONS[sub_type]] else: form.mold_specific_type.choices = [('', 'Select Specific Type')] # Check if specific type is needed if sub_type and sub_type in form.MOLD_SPECIFIC_TYPE_OPTIONS: # If specific type is needed but not provided, set it to empty string if not request.form.get('mold_specific_type'): form.mold_specific_type.data = '' else: # If specific type is not needed, set it to empty string form.mold_specific_type.data = '' if not form.validate_on_submit(): current_app.logger.warning('Form validation failed:') current_app.logger.warning(f'Form errors: {form.errors}') current_app.logger.warning(f'Form data after validation: {form.data}') current_app.logger.warning(f'Request form data: {dict(request.form)}') return jsonify({ 'success': False, 'error': 'Invalid form data', 'message': 'Please check the form for errors', 'errors': form.errors, 'form_data': dict(request.form) }), 400 # Get form data form_data = form.data current_app.logger.info(f'Processing form data: {form_data}') # Calculate costs try: result = calculate_base_price( mold_main_type=form.mold_main_type.data, mold_sub_type=form.mold_sub_type.data, mold_specific_type=form.mold_specific_type.data, sliders_count=int(form.sliders_count.data), cavity_count=float(form.cavity_count.data), digital_texture_type=form.digital_texture_type.data, complexity_high_sidewall=(form.complexity_high_sidewall.data == 'yes') ) current_app.logger.info(f'Calculation result: {result}') # Get cost factors for tax rate cost_factors, tax_rates, default_net_base = get_cost_factors() # Get tax rate for the country tax_rate = tax_rates.get(form.mold_shop_country.data, 0.0) # Calculate total cost (base price + tax) total_cost = result['total_cost'] * (1 + tax_rate) return jsonify({ 'success': True, 'base_price': float(result['base_price']), 'complexity_cost': float(result['complexity_cost']), 'texture_cost': float(result['texture_cost']), 'tax_rate': float(tax_rate), 'total_cost': float(total_cost), 'mold_shop_name': form.mold_shop_name.data, 'mold_shop_country': form.mold_shop_country.data, 't1_shoe_factory_name': form.t1_shoe_factory_name.data, 't1_shoe_factory_country': form.t1_shoe_factory_country.data, 't2_component_factory_name': form.t2_component_factory_name.data, 't2_component_factory_country': form.t2_component_factory_country.data, 'model_name': form.model_name.data, 'tooling_id': form.tooling_id.data, 'season': form.season.data, 'stage': form.stage.data, 'type': form.type.data, 'issue_date': form.issue_date.data.strftime('%Y-%m-%d'), 'approved_by_mold_shop': form.approved_by_mold_shop.data, 'approved_by_t1_tooling': form.approved_by_t1_tooling.data, 'approved_by_lo_tooling': form.approved_by_lo_tooling.data, 'mold_main_type': form.mold_main_type.data, 'mold_sub_type': form.mold_sub_type.data, 'mold_specific_type': form.mold_specific_type.data, 'complexity_high_sidewall': 'yes' if form.complexity_high_sidewall.data == 'yes' else 'no', 'sliders_count': form.sliders_count.data, 'cavity_count': form.cavity_count.data, 'digital_texture_type': form.digital_texture_type.data }) except Exception as calc_error: current_app.logger.error(f'Error calculating costs: {str(calc_error)}', exc_info=True) return jsonify({ 'success': False, 'error': 'Calculation error', 'message': str(calc_error) }), 500 except Exception as form_error: current_app.logger.error(f'Error processing form: {str(form_error)}', exc_info=True) return jsonify({ 'success': False, 'error': 'Form processing error', 'message': str(form_error) }), 500 # GET request - render the form return render_template('calculator.html', form=form) except Exception as e: current_app.logger.error(f'Unexpected error in calculate route: {str(e)}', exc_info=True) return jsonify({ 'success': False, 'error': 'Unexpected error', 'message': str(e) }), 500 @bp.route('/quotations') @login_required @limiter.limit("30 per minute") @cache.cached(timeout=60, key_prefix='user_quotations') def quotations(): quotations = Quotation.query.filter_by(user_id=current_user.id)\ .order_by(Quotation.created_at.desc()).all() return render_template('quotations.html', quotations=quotations) @bp.route('/quotation/') @login_required @limiter.limit("30 per minute") @cache.cached(timeout=60, key_prefix='quotation_detail') def quotation_detail(id): quotation = Quotation.query.get_or_404(id) if quotation.user_id != current_user.id and not current_user.is_admin: flash('Access denied.', 'danger') return redirect(url_for('main.quotations')) return render_template('quotation_detail.html', quotation=quotation) @bp.route('/health') def health_check(): """Health check endpoint that returns the status of various system components.""" # Get health status health_status = get_health_status() # Set appropriate status code based on overall health status_code = 200 if health_status['status'] == 'healthy' else 503 # Add rate limiting headers response = jsonify(health_status) response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' response.headers['Pragma'] = 'no-cache' response.headers['Expires'] = '0' return response, status_code @bp.route('/api/recent-quotes') @login_required @limiter.limit("30 per minute") def recent_quotes(): try: recent_quotes = Quotation.query.filter_by(user_id=current_user.id)\ .order_by(Quotation.created_at.desc())\ .limit(5)\ .all() return jsonify({ 'success': True, 'quotes': [{ 'id': quote.id, 'model_name': quote.model_name, 'tooling_id': quote.tooling_id, 'mold_sub_type': quote.mold_sub_type, 'cavity_count': float(quote.cavity_count), 'sliders_count': quote.sliders_count, 'created_at': quote.created_at.strftime('%Y-%m-%d %H:%M'), 'mold_shop_country': quote.mold_shop_country, 'digital_texture_type': quote.digital_texture_type, 'season': quote.season, 'stage': quote.stage, 'base_price': float(quote.base_price), 'tax_rate': float(quote.tax_rate) if quote.tax_rate is not None else 0.0, 'total_cost': float(quote.total_cost) } for quote in recent_quotes] }) except Exception as e: current_app.logger.error(f'Error fetching recent quotes: {str(e)}', exc_info=True) return jsonify({ 'success': False, 'error': 'An error occurred while fetching recent quotes', 'quotes': [] }), 500 @bp.route('/save-quotation', methods=['POST']) @login_required @limiter.limit("30 per minute") def save_quotation(): """Save a quotation to the database""" try: # Get form data form_data = request.get_json() if not form_data: return jsonify({ 'success': False, 'error': 'No data provided' }), 400 # Check for duplicate submission (same user, same data within last 30 seconds) try: recent_duplicate = Quotation.query.filter( Quotation.user_id == current_user.id, Quotation.model_name == form_data.get('model_name'), Quotation.tooling_id == form_data.get('tooling_id'), Quotation.mold_main_type == form_data.get('mold_main_type'), Quotation.mold_sub_type == form_data.get('mold_sub_type'), Quotation.created_at >= datetime.now(timezone.utc) - timedelta(seconds=30) ).first() if recent_duplicate: current_app.logger.warning(f'Duplicate submission detected for user {current_user.id}, tooling_id {form_data.get("tooling_id")}') return jsonify({ 'success': False, 'error': 'Duplicate submission', 'message': 'This quotation was already submitted recently. Please wait a moment before trying again.', 'quotation_id': recent_duplicate.id }), 409 except Exception as duplicate_check_error: current_app.logger.error(f'Error checking for duplicates: {str(duplicate_check_error)}') # Continue with submission even if duplicate check fails # Ensure mold_specific_type is not empty or missing mold_specific_type = form_data.get('mold_specific_type') if not mold_specific_type: mold_specific_type = 'N/A' # Create new quotation quotation = Quotation( user_id=current_user.id, mold_shop_name=form_data.get('mold_shop_name'), mold_shop_country=form_data.get('mold_shop_country'), t1_shoe_factory_name=form_data.get('t1_shoe_factory_name'), t1_shoe_factory_country=form_data.get('t1_shoe_factory_country'), t2_component_factory_name=form_data.get('t2_component_factory_name'), t2_component_factory_country=form_data.get('t2_component_factory_country'), model_name=form_data.get('model_name'), tooling_id=form_data.get('tooling_id'), season=form_data.get('season'), stage=form_data.get('stage'), type=form_data.get('type'), issue_date=datetime.strptime(form_data.get('issue_date'), '%Y-%m-%d').date(), approved_by_mold_shop=form_data.get('approved_by_mold_shop'), approved_by_t1_tooling=form_data.get('approved_by_t1_tooling'), approved_by_lo_tooling=form_data.get('approved_by_lo_tooling'), mold_main_type=form_data.get('mold_main_type'), mold_sub_type=form_data.get('mold_sub_type'), mold_specific_type=mold_specific_type, complexity_high_sidewall=(form_data.get('complexity_high_sidewall') == 'yes'), sliders_count=int(form_data.get('sliders_count')), cavity_count=float(form_data.get('cavity_count')), digital_texture_type=form_data.get('digital_texture_type'), base_price=float(form_data.get('base_price')), tax_rate=float(form_data.get('tax_rate')), total_cost=float(form_data.get('total_cost')) ) # Save to database db.session.add(quotation) db.session.commit() current_app.logger.info(f'Quotation saved with ID: {quotation.id}') return jsonify({ 'success': True, 'quotation_id': quotation.id, 'message': 'Quotation saved successfully' }) except Exception as e: db.session.rollback() current_app.logger.error(f'Error saving quotation: {str(e)}', exc_info=True) return jsonify({ 'success': False, 'error': 'Failed to save quotation', 'message': str(e) }), 500