from app import db # Add this if not already there from sqlalchemy import func, extract # Add this from flask import Blueprint, request, jsonify, send_from_directory from app.services.services import MachineService, UserService, ProductService, serial_service, TransactionService, RoleService, BranchService, BrandService, CategoryService, SubCategoryService from app.models.models import Machine, User, Transaction, Product, VendingSlot, Role import os import hashlib import time import uuid import json from dotenv import load_dotenv from functools import wraps import jwt # Added for JWT token generation from datetime import datetime, timedelta # Added for token expiration load_dotenv() # Load environment variables bp = Blueprint('api', __name__) @bp.route('/login', methods=['POST']) def login(): data = request.json machine_id = data.get('machine_id') password = data.get('password') if not machine_id or not password: return jsonify({"error": "machine_id and password are required"}), 400 machine = Machine.query.filter_by(machine_id=machine_id).first() print(f"Debug - Machine found: {machine}") print(f"Debug - Machine ID from DB: {machine.machine_id if machine else 'None'}") print(f"Debug - Password from DB: {machine.password if machine else 'None'}") print(f"Debug - Input Password: {password}") if not machine or machine.password != password: return jsonify({"error": "Invalid machine_id or password"}), 401 return jsonify({ 'message': 'Login successful', 'machine_id': machine.machine_id, 'id': machine.id }), 200 @bp.route('/machine-slots/', methods=['GET']) def get_machine_slots(machine_id): slots = MachineService.get_machine_slots(machine_id) if slots is None: return jsonify({"error": "Machine not found"}), 404 return jsonify(slots) @bp.route('/machine-slots/update-inventory', methods=['POST']) def update_machine_slots_inventory(): """Update inventory after successful purchase""" try: print("\n" + "=" * 60) print("INVENTORY UPDATE REQUEST") print("=" * 60) data = request.json machine_id = data.get('machineId') inventory_updates = data.get('inventoryUpdates', []) if not machine_id or not inventory_updates: return jsonify({'error': 'Missing machineId or inventoryUpdates'}), 400 print(f"Machine ID: {machine_id}") print(f"Updates: {len(inventory_updates)} items") # Update each slot for update in inventory_updates: slot_id = update.get('slotId') quantity_dispensed = update.get('quantityDispensed') # Extract row and column from slotId (e.g., "A1" -> row="A", column=1) row_id = slot_id[0] column = int(slot_id[1:]) print(f" - Updating slot {slot_id}: reducing {quantity_dispensed} units") # Find and update the slot slot = VendingSlot.query.filter_by( machine_id=machine_id, row_id=row_id, slot_name=slot_id ).first() if slot: # Reduce units new_units = max(0, slot.units - quantity_dispensed) slot.units = new_units # Disable slot if units reach 0 if new_units <= 0: slot.enabled = False print(f" ✓ Slot {slot_id}: {slot.units} -> {new_units} units (DISABLED)") else: print(f" ✓ Slot {slot_id}: {slot.units + quantity_dispensed} -> {new_units} units") else: print(f" ✗ Slot {slot_id} not found!") # Commit all changes db.session.commit() print(f"✓ Inventory updated successfully") print("=" * 60 + "\n") return jsonify({ 'success': True, 'message': 'Inventory updated successfully', 'updated_slots': len(inventory_updates) }), 200 except Exception as e: db.session.rollback() print(f"\n✗ INVENTORY UPDATE ERROR") print(f"Error: {str(e)}") import traceback traceback.print_exc() print("=" * 60 + "\n") return jsonify({ 'success': False, 'error': str(e) }), 500 @bp.route('/uploads/') def uploaded_file(filename): return send_from_directory(os.path.join(bp.root_path, '..', 'Uploads'), filename) @bp.route('/machines', methods=['GET']) def get_machines(): try: machines = MachineService.get_all_machines() return jsonify([machine.to_dict() for machine in machines]) except Exception as e: return jsonify({"error": str(e)}), 500 @bp.route('/machines', methods=['POST']) def add_machine(): data = request.json # Get current user from token current_user = get_current_user_from_token() if not current_user: return jsonify({"error": "Authentication required"}), 401 try: # FORCE client_id for Clients if current_user.roles == 'Client': data['client_id'] = current_user.id print(f"✓ Client {current_user.username} (ID: {current_user.id}) creating machine") else: # Admin/Management must provide client_id if 'client_id' not in data: return jsonify({"error": "client_id is required"}), 400 print(f"✓ Admin creating machine for client_id: {data['client_id']}") # ADD CREATED_BY data['created_by'] = current_user.id new_machine = MachineService.create_machine(data) print(f"✓ Machine created: {new_machine.machine_id} for client_id: {new_machine.client_id}") print(f"✓ Created by: {current_user.username} (ID: {current_user.id})") return jsonify(new_machine.to_dict()), 201 except ValueError as e: return jsonify({"error": str(e)}), 400 except Exception as e: import traceback traceback.print_exc() return jsonify({"error": str(e)}), 500 @bp.route('/machines/', methods=['PUT']) def update_machine(id): data = request.json try: updated_machine = MachineService.update_machine(id, data) return jsonify(updated_machine.to_dict()) except ValueError as e: return jsonify({"error": str(e)}), 400 except Exception as e: return jsonify({"error": str(e)}), 500 @bp.route('/machines/', methods=['DELETE']) def delete_machine(id): try: MachineService.delete_machine(id) return jsonify({"message": "Machine deleted successfully"}), 200 except Exception as e: return jsonify({"error": str(e)}), 500 @bp.route('/refillers//machines', methods=['GET']) def get_refiller_machines(refiller_id): """Get all machines assigned to a Refiller""" try: current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 # Check permissions is_admin = current_user.roles in ['Management', 'SuperAdmin', 'Admin'] is_own_data = current_user.id == refiller_id is_client_checking_own_refiller = False if current_user.roles == 'Client': # Client can view their assigned Refiller's machines refiller = User.query.get(refiller_id) if refiller and refiller.assigned_to == current_user.id: is_client_checking_own_refiller = True if not (is_admin or is_own_data or is_client_checking_own_refiller): return jsonify({'error': 'Permission denied'}), 403 # Get machines machines = UserService.get_refiller_machines(refiller_id) return jsonify([machine.to_dict() for machine in machines]), 200 except Exception as e: import traceback traceback.print_exc() return jsonify({'error': str(e)}), 500 @bp.route('/refillers//machines', methods=['POST']) def assign_machines_to_refiller(refiller_id): """Assign machines to a Refiller""" try: current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 # Only admins can assign machines if current_user.roles not in ['Management', 'SuperAdmin', 'Admin']: return jsonify({'error': 'Permission denied'}), 403 data = request.json machine_ids = data.get('machine_ids', []) if not machine_ids: return jsonify({'error': 'machine_ids array required'}), 400 # Assign machines assignments = UserService.assign_machines_to_refiller( refiller_id, machine_ids, current_user.id ) return jsonify({ 'message': f'Successfully assigned {len(assignments)} machines', 'assignments': [a.to_dict() for a in assignments] }), 200 except ValueError as e: return jsonify({'error': str(e)}), 400 except Exception as e: import traceback traceback.print_exc() return jsonify({'error': str(e)}), 500 @bp.route('/refillers//machines', methods=['PUT']) def update_refiller_machines(refiller_id): """Update all machine assignments for a Refiller (replaces existing)""" try: current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 # Only admins can update assignments if current_user.roles not in ['Management', 'SuperAdmin', 'Admin']: return jsonify({'error': 'Permission denied'}), 403 data = request.json machine_ids = data.get('machine_ids', []) # Update machines (replaces all existing) assignments = UserService.update_refiller_machines( refiller_id, machine_ids, current_user.id ) return jsonify({ 'message': f'Successfully updated machine assignments', 'assignments': [a.to_dict() for a in assignments], 'count': len(assignments) }), 200 except ValueError as e: return jsonify({'error': str(e)}), 400 except Exception as e: import traceback traceback.print_exc() return jsonify({'error': str(e)}), 500 @bp.route('/refillers//machines/', methods=['DELETE']) def remove_machine_from_refiller(refiller_id, machine_id): """Remove a machine assignment from a Refiller""" try: current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 # Only admins can remove assignments if current_user.roles not in ['Management', 'SuperAdmin', 'Admin']: return jsonify({'error': 'Permission denied'}), 403 success = UserService.remove_machine_from_refiller(refiller_id, machine_id) if success: return jsonify({ 'message': f'Successfully removed machine {machine_id} from refiller' }), 200 else: return jsonify({'error': 'Assignment not found'}), 404 except Exception as e: import traceback traceback.print_exc() return jsonify({'error': str(e)}), 500 # ============================================================================ # MACHINE-CENTRIC ENDPOINTS # ============================================================================ @bp.route('/machines//refillers', methods=['GET']) def get_machine_refillers(machine_id): """Get all Refillers assigned to a Machine""" try: current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 # Admins and machine owner can view machine = Machine.query.filter_by(machine_id=machine_id).first() if not machine: return jsonify({'error': 'Machine not found'}), 404 is_admin = current_user.roles in ['Management', 'SuperAdmin', 'Admin'] is_owner = machine.client_id == current_user.id if not (is_admin or is_owner): return jsonify({'error': 'Permission denied'}), 403 # Get refillers refillers = MachineService.get_machine_refillers(machine_id) return jsonify([refiller.to_dict() for refiller in refillers]), 200 except Exception as e: import traceback traceback.print_exc() return jsonify({'error': str(e)}), 500 @bp.route('/machines//refillers', methods=['POST']) def assign_refillers_to_machine(machine_id): """Assign Refillers to a Machine""" try: current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 # Only admins can assign refillers to machines if current_user.roles not in ['Management', 'SuperAdmin', 'Admin']: return jsonify({'error': 'Permission denied'}), 403 data = request.json refiller_ids = data.get('refiller_ids', []) if not refiller_ids: return jsonify({'error': 'refiller_ids array required'}), 400 # Assign refillers assignments = MachineService.assign_refillers_to_machine( machine_id, refiller_ids, current_user.id ) return jsonify({ 'message': f'Successfully assigned {len(assignments)} refillers', 'assignments': [a.to_dict() for a in assignments] }), 200 except ValueError as e: return jsonify({'error': str(e)}), 400 except Exception as e: import traceback traceback.print_exc() return jsonify({'error': str(e)}), 500 # ============================================================================ # UTILITY ENDPOINTS # ============================================================================ @bp.route('/clients//machines', methods=['GET']) def get_client_machines(client_id): """Get all machines belonging to a Client (for assignment dropdown)""" try: current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 # Admins or the client themselves can view is_admin = current_user.roles in ['Management', 'SuperAdmin', 'Admin'] is_own_data = current_user.id == client_id if not (is_admin or is_own_data): return jsonify({'error': 'Permission denied'}), 403 # Get client's machines machines = Machine.query.filter_by(client_id=client_id).all() return jsonify([{ 'id': m.id, 'machine_id': m.machine_id, 'machine_model': m.machine_model, 'machine_type': m.machine_type, 'branch_name': m.branch_name, 'operation_status': m.operation_status } for m in machines]), 200 except Exception as e: import traceback traceback.print_exc() return jsonify({'error': str(e)}), 500 @bp.route('/refillers//available-machines', methods=['GET']) def get_available_machines_for_refiller(refiller_id): """Get machines available to assign to a Refiller (from their assigned Client)""" try: current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 # Only admins can view if current_user.roles not in ['Management', 'SuperAdmin', 'Admin']: return jsonify({'error': 'Permission denied'}), 403 # Get refiller refiller = User.query.get_or_404(refiller_id) if refiller.roles != 'Refiller': return jsonify({'error': 'User is not a Refiller'}), 400 # If refiller is assigned to a client, get that client's machines # Otherwise, get all machines if refiller.assigned_to: machines = Machine.query.filter_by(client_id=refiller.assigned_to).all() message = f"Machines from assigned client" else: machines = Machine.query.all() message = "All machines (no client assigned)" return jsonify({ 'message': message, 'machines': [{ 'id': m.id, 'machine_id': m.machine_id, 'machine_model': m.machine_model, 'machine_type': m.machine_type, 'branch_name': m.branch_name, 'client_name': m.client.username if m.client else m.client_name, 'operation_status': m.operation_status } for m in machines] }), 200 except Exception as e: import traceback traceback.print_exc() return jsonify({'error': str(e)}), 500 @bp.route('/clients', methods=['GET']) def get_clients(): """Get all clients for assignment dropdown""" try: current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 # Only Management, SuperAdmin, and Admin can see clients if current_user.roles not in ['Management', 'SuperAdmin', 'Admin']: return jsonify({'error': 'Permission denied'}), 403 # Get all users with Client role clients = User.query.filter_by(roles='Client', user_status='Active').all() client_list = [{ 'id': client.id, 'user_id': client.user_id, 'username': client.username, 'email': client.email, 'contact': client.contact } for client in clients] print(f"✓ Retrieved {len(client_list)} active clients") return jsonify(client_list), 200 except Exception as e: import traceback traceback.print_exc() return jsonify({'error': str(e)}), 500 @bp.route('/users//assigned-refillers', methods=['GET']) def get_assigned_refillers(user_id): """Get all refillers assigned to a specific client""" try: current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 # Check permissions is_admin = current_user.roles in ['Management', 'SuperAdmin', 'Admin'] is_own_data = current_user.id == user_id if not (is_admin or is_own_data): return jsonify({'error': 'Permission denied'}), 403 # Get user and validate it's a Client user = User.query.get_or_404(user_id) if user.roles != 'Client': return jsonify({'error': 'User is not a Client'}), 400 # Get all refillers assigned to this client refillers = User.query.filter_by( assigned_to=user_id, roles='Refiller', user_status='Active' ).all() refiller_list = [{ 'id': refiller.id, 'user_id': refiller.user_id, 'username': refiller.username, 'email': refiller.email, 'contact': refiller.contact, 'created_by_username': refiller.creator.username if refiller.creator else None } for refiller in refillers] print(f"✓ Client {user.username} has {len(refiller_list)} assigned refillers") return jsonify(refiller_list), 200 except Exception as e: import traceback traceback.print_exc() return jsonify({'error': str(e)}), 500 # Updated User Routes in routes.py # In your routes.py file @bp.route('/users', methods=['GET']) def get_users(): try: users = UserService.get_all_users() return jsonify([user.to_dict() for user in users]) except Exception as e: return jsonify({"error": str(e)}), 500 @bp.route('/users', methods=['POST']) def add_users(): print("\n" + "=" * 60) print("CREATE USER REQUEST") print("=" * 60) # Get current user current_user = get_current_user_from_token() if not current_user: return jsonify({"error": "Authentication required"}), 401 try: # Get form data data = request.form.to_dict() # ADD CREATED_BY data['created_by'] = current_user.id # ⭐ NEW: Handle assigned_to if 'assigned_to' in data: assigned_to = data.get('assigned_to') if assigned_to: try: data['assigned_to'] = int(assigned_to) except (ValueError, TypeError): return jsonify({"error": "Invalid assigned_to value"}), 400 else: data['assigned_to'] = None print("Form data keys:", list(data.keys())) print("Form values:", data) # Get files photo_file = request.files.get('photo') logo_file = request.files.get('company_logo') document_files = request.files.getlist('documents') documents_metadata = request.form.get('documents_metadata') print(f"\nFiles received:") print(f" Photo: {photo_file.filename if photo_file else None}") print(f" Logo: {logo_file.filename if logo_file else None}") print(f" Documents: {len(document_files)} files") # Call service print("\nCalling UserService.create_user...") new_user = UserService.create_user( data, photo_file, logo_file, document_files if document_files else None, documents_metadata ) print(f"✓ User created successfully by: {current_user.username}") if new_user.assigned_to: print(f"✓ Assigned to Client ID: {new_user.assigned_to}") print("=" * 60 + "\n") return jsonify(new_user.to_dict()), 201 except ValueError as e: print(f"\n✗ Validation Error: {str(e)}") print("=" * 60 + "\n") return jsonify({"error": str(e)}), 400 except Exception as e: print(f"\n✗ SERVER ERROR") print(f"Error type: {type(e).__name__}") print(f"Error message: {str(e)}") import traceback print("\nFull traceback:") traceback.print_exc() print("=" * 60 + "\n") return jsonify({"error": f"Server error: {str(e)}"}), 500 # UPDATE the existing update_user endpoint to handle assigned_to @bp.route('/users/', methods=['PUT']) def update_user(id): try: data = request.form.to_dict() # ⭐ NEW: Handle assigned_to if 'assigned_to' in data: assigned_to = data.get('assigned_to') if assigned_to: try: data['assigned_to'] = int(assigned_to) except (ValueError, TypeError): return jsonify({"error": "Invalid assigned_to value"}), 400 else: data['assigned_to'] = None # Get uploaded files photo_file = request.files.get('photo') logo_file = request.files.get('company_logo') document_files = request.files.getlist('documents') # Get document metadata documents_metadata = request.form.get('documents_metadata') # Pass metadata to service updated_user = UserService.update_user( id, data, photo_file, logo_file, document_files, documents_metadata ) return jsonify(updated_user.to_dict()) except ValueError as e: return jsonify({"error": str(e)}), 400 except Exception as e: return jsonify({"error": str(e)}), 500 @bp.route('/users/', methods=['DELETE']) def delete_user(id): try: UserService.delete_user(id) return jsonify({"message": "User deleted successfully"}), 200 except Exception as e: return jsonify({"error": str(e)}), 500 @bp.route('/users//documents/', methods=['DELETE']) def delete_user_document(user_id, document_path): """Delete a specific document from a user""" try: # Decode the document path (in case it was URL encoded) document_path = '/' + document_path success = UserService.delete_user_document(user_id, document_path) if success: return jsonify({"message": "Document deleted successfully"}), 200 else: return jsonify({"error": "Document not found"}), 404 except Exception as e: return jsonify({"error": str(e)}), 500 # Serve uploaded files @bp.route('/uploads/user_photos/') def serve_user_photo(filename): return send_from_directory(os.path.join(bp.root_path, '..', 'Uploads', 'user_photos'), filename) @bp.route('/uploads/company_logos/') def serve_company_logo(filename): return send_from_directory(os.path.join(bp.root_path, '..', 'Uploads', 'company_logos'), filename) @bp.route('/uploads/user_documents/') def serve_user_document(filename): return send_from_directory(os.path.join(bp.root_path, '..', 'Uploads', 'user_documents'), filename) @bp.route('/products', methods=['GET']) def get_products(): try: products = ProductService.get_all_products() return jsonify([product.to_dict() for product in products]) except Exception as e: return jsonify({"error": str(e)}), 500 @bp.route('/products', methods=['POST']) def add_product(): # Get current user current_user = get_current_user_from_token() if not current_user: return jsonify({"error": "Authentication required"}), 401 try: data = request.form.to_dict() # ADD CREATED_BY data['created_by'] = current_user.id file = request.files.get('product_image') new_product = ProductService.create_product(data, file) print(f"✓ Product created by: {current_user.username}") return jsonify(new_product.to_dict()), 201 except ValueError as e: return jsonify({"error": str(e)}), 400 except Exception as e: return jsonify({"error": str(e)}), 500 @bp.route('/products/', methods=['PUT']) def update_product(id): try: data = request.form file = request.files.get('product_image') if 'product_image' in request.files else None updated_product = ProductService.update_product(id, data, file) return jsonify(updated_product.to_dict()) except ValueError as e: return jsonify({"error": str(e)}), 400 except Exception as e: return jsonify({"error": str(e)}), 500 @bp.route('/products/', methods=['DELETE']) def delete_product(id): try: ProductService.delete_product(id) return jsonify({"message": "Product deleted successfully"}), 200 except Exception as e: return jsonify({"error": str(e)}), 500 @bp.route('/machines/', methods=['GET']) def get_machine(id): try: machine = MachineService.get_machine_by_id(id) return jsonify(machine.to_dict()) except Exception as e: return jsonify({"error": str(e)}), 404 @bp.route('/machines/', methods=['GET']) def get_vending_state(machine_id): try: state = MachineService.get_vending_state(machine_id) return jsonify(state) except Exception as e: return jsonify({"error": str(e)}), 404 @bp.route('/machines//vending-state', methods=['PUT']) def update_vending_state(machine_id): try: data = request.get_json() if not data or 'vendingRows' not in data: return jsonify({'error': 'Invalid data'}), 400 result = MachineService.update_vending_state(machine_id, data['vendingRows']) return jsonify(result) except Exception as e: return jsonify({"error": str(e)}), 500 @bp.route('/create-payu-order', methods=['POST']) def create_payu_order(): data = request.json or {} machine_id = data.get('machineId') total_amount = data.get('totalAmount') product_info = data.get('productInfo') cart_data = data.get('cartData', '[]') user_details = data.get('userDetails', {}) if not machine_id or not total_amount or not product_info: return jsonify({"error": "Missing required fields"}), 400 txnid = f"txn_{int(time.time())}_{uuid.uuid4().hex[:5]}" payu_key = os.getenv('PAYU_MERCHANT_KEY') payu_salt = os.getenv('PAYU_MERCHANT_SALT') if not payu_key or not payu_salt: return jsonify({"error": "PayU credentials not configured"}), 500 payu_data = { "key": payu_key, "txnid": txnid, "amount": f"{float(total_amount):.2f}", "productinfo": product_info, "firstname": user_details.get("firstname", "Customer"), "email": user_details.get("email", "customer@example.com"), "phone": user_details.get("phone", "9999999999"), "surl": f"{os.getenv('BASE_URL', 'http://localhost:5000')}/payment-success", "furl": f"{os.getenv('BASE_URL', 'http://localhost:5000')}/payment-failure", "service_provider": "payu_paisa", "udf1": machine_id or "", "udf2": cart_data or "", "udf3": "", "udf4": "", "udf5": "" } # Correct PayU hash string hash_string = ( f"{payu_data['key']}|{payu_data['txnid']}|{payu_data['amount']}|" f"{payu_data['productinfo']}|{payu_data['firstname']}|{payu_data['email']}|" f"{payu_data['udf1']}|{payu_data['udf2']}|{payu_data['udf3']}|" f"{payu_data['udf4']}|{payu_data['udf5']}||||||{payu_salt}" ) # Calculate hash as a simple string (NOT an object) calculated_hash = hashlib.sha512(hash_string.encode('utf-8')).hexdigest() payu_data["hash"] = calculated_hash # Just the string, not an object # Debug logging print("=== PayU Hash Debug ===") print(f"Hash string: {hash_string}") print(f"Calculated hash: {calculated_hash}") print(f"PayU data: {payu_data}") print("======================") return jsonify(payu_data), 200 @bp.route('/create-payu-order-v2', methods=['POST']) def create_payu_order_v2(): """Alternative PayU order endpoint with JSON hash format""" data = request.json if not data: return jsonify({"error": "No data received"}), 400 machine_id = data.get('machineId') total_amount = data.get('totalAmount') product_info = data.get('productInfo') cart_data = data.get('cartData', '[]') user_details = data.get('userDetails', {}) if not machine_id or not total_amount or not product_info: return jsonify({"error": "Missing required fields"}), 400 machine = Machine.query.filter_by(machine_id=machine_id).first() if not machine: return jsonify({"error": "Machine not found"}), 404 txnid = f"txn_{int(time.time())}_{uuid.uuid4().hex[:5]}" payu_key = os.getenv('PAYU_MERCHANT_KEY') payu_salt = os.getenv('PAYU_MERCHANT_SALT') if not payu_key or not payu_salt: return jsonify({"error": "PayU credentials not configured"}), 500 payu_data = { "key": payu_key, "txnid": txnid, "amount": f"{float(total_amount):.2f}", "productinfo": product_info, "firstname": "mukesh", # Consistent with error message "email": "customer@example.com", "phone": "9999999999", "surl": "https://savannah-syntypic-synchronously.ngrok-free.dev/payment-success", "furl": "https://savannah-syntypic-synchronously.ngrok-free.dev/payment-failure", "service_provider": "payu_paisa", "udf1": machine_id, "udf2": cart_data, "udf3": "", "udf4": "", "udf5": "" } hash_string = f"{payu_data['key']}|{payu_data['txnid']}|{payu_data['amount']}|{payu_data['productinfo']}|{payu_data['firstname']}|{payu_data['email']}|{payu_data['udf1']}|{payu_data['udf2']}|{payu_data['udf3']}|{payu_data['udf4']}|{payu_data['udf5']}||||||{payu_salt}" calculated_hash = hashlib.sha512(hash_string.encode()).hexdigest() # Return hash in JSON format as shown in PayU error message hash_json = json.dumps({ "v1": calculated_hash, "v2": calculated_hash }) payu_data["hash"] = hash_json print("=" * 50) print("PayU V2 Order Creation (JSON Hash Format)") print("=" * 50) print(f"Hash string: {hash_string}") print(f"Calculated hash: {calculated_hash}") print(f"JSON hash format: {hash_json}") print("=" * 50) return jsonify(payu_data), 200 @bp.route('/success', methods=['POST']) def payment_success(): """Handle PayU success callback with proper verification""" data = request.form # Extract all PayU response parameters status = data.get('status') txnid = data.get('txnid') amount = data.get('amount') productinfo = data.get('productinfo') firstname = data.get('firstname') email = data.get('email') phone = data.get('phone') udf1 = data.get('udf1', '') # machine_id udf2 = data.get('udf2', '') # cart_data udf3 = data.get('udf3', '') udf4 = data.get('udf4', '') udf5 = data.get('udf5', '') hash_received = data.get('hash') payuMoneyId = data.get('payuMoneyId') print("=" * 50) print("Payment Success Callback") print("=" * 50) print(f"Status: {status}") print(f"Transaction ID: {txnid}") print(f"Amount: {amount}") print(f"Machine ID (UDF1): {udf1}") print(f"Cart Data (UDF2): {udf2}") print(f"PayU Money ID: {payuMoneyId}") print("=" * 50) # Verify hash for security (PayU response hash verification) payu_salt = os.getenv('PAYU_MERCHANT_SALT') payu_key = os.getenv('PAYU_MERCHANT_KEY') if payu_salt and payu_key: # Response hash formula: sha512(SALT|status||||||udf5|udf4|udf3|udf2|udf1|email|firstname|productinfo|amount|txnid|key) response_hash_string = f"{payu_salt}|{status}||||||{udf5}|{udf4}|{udf3}|{udf2}|{udf1}|{email}|{firstname}|{productinfo}|{amount}|{txnid}|{payu_key}" expected_hash = hashlib.sha512(response_hash_string.encode()).hexdigest() print(f"Response Hash String: {response_hash_string}") print(f"Expected Hash: {expected_hash}") print(f"Received Hash: {hash_received}") if hash_received != expected_hash: print("WARNING: Hash verification failed!") # In production, you might want to reject this transaction # For now, we'll log the warning but continue # Process successful payment if status == 'success': try: # Parse cart data for dispensing instructions if udf2: cart_items = json.loads(udf2) print(f"Cart items to dispense: {cart_items}") # TODO: Here you would typically: # 1. Save transaction to database # 2. Send dispensing commands to vending machine # 3. Update inventory # 4. Send confirmation notifications # Example dispensing logic (implement according to your machine interface) for item in cart_items: slot_id = item.get('slotId') quantity = item.get('quantity', 1) product_name = item.get('productName') print(f"Dispensing: {quantity}x {product_name} from slot {slot_id}") # Call your machine service to dispense # MachineService.dispense_product(udf1, slot_id, quantity) except json.JSONDecodeError as e: print(f"Error parsing cart data: {e}") except Exception as e: print(f"Error processing dispensing: {e}") # Return response return jsonify({ "message": "Payment processed successfully" if status == 'success' else "Payment completed", "txnid": txnid, "status": status, "amount": amount, "payuMoneyId": payuMoneyId }), 200 @bp.route('/failure', methods=['POST']) def payment_failure(): """Handle PayU failure callback""" data = request.form txnid = data.get('txnid') status = data.get('status') error_message = data.get('error', 'Payment failed') amount = data.get('amount') udf1 = data.get('udf1', '') # machine_id print("=" * 50) print("Payment Failure Callback") print("=" * 50) print(f"Transaction ID: {txnid}") print(f"Status: {status}") print(f"Error: {error_message}") print(f"Amount: {amount}") print(f"Machine ID: {udf1}") print("=" * 50) # TODO: Here you would typically: # 1. Log the failed transaction # 2. Send failure notification # 3. Release any reserved inventory return jsonify({ "error": "Payment failed", "txnid": txnid, "status": status, "message": error_message, "amount": amount }), 400 @bp.route('/api/verify-token', methods=['POST']) def verify_token(): """ Verify JWT token endpoint """ try: data = request.json token = data.get('token') if not token: return jsonify({"error": "Token required"}), 400 secret_key = os.getenv('SECRET_KEY') if not secret_key: return jsonify({"error": "Server configuration error"}), 500 try: payload = jwt.decode(token, secret_key, algorithms=['HS256']) user_id = payload.get('user_id') user = User.query.get(user_id) if not user: return jsonify({"error": "User not found"}), 404 return jsonify({ 'valid': True, 'user': { 'id': user.id, 'user_id': user.user_id, 'username': user.username, 'email': user.email, 'roles': user.roles, 'user_status': user.user_status, 'photo': user.photo, 'company_logo': user.company_logo } }), 200 except jwt.ExpiredSignatureError: return jsonify({"error": "Token expired"}), 401 except jwt.InvalidTokenError: return jsonify({"error": "Invalid token"}), 401 except Exception as e: return jsonify({"error": str(e)}), 500 @bp.route('/api/user-login', methods=['POST']) def user_login(): """ User login endpoint with proper password hashing validation Accepts email OR username along with password """ try: data = request.json if not data: return jsonify({"error": "No data provided"}), 400 identifier = data.get('email') # Can be username or email password = data.get('password') # Validate required fields if not identifier or not password: return jsonify({"error": "Email/Username and password are required"}), 400 # Query user by email OR username user = User.query.filter( (User.email == identifier) | (User.username == identifier) ).first() # Debug logging print(f"\n{'='*60}") print(f"LOGIN ATTEMPT") print(f"{'='*60}") print(f"Identifier provided: {identifier}") print(f"User found: {user is not None}") if user: print(f"User ID: {user.id}") print(f"Username: {user.username}") print(f"Email: {user.email}") print(f"Password hash exists: {bool(user.password)}") print(f"Password hash length: {len(user.password) if user.password else 0}") print(f"{'='*60}\n") # Check if user exists and password is correct if not user: print("Login failed: User not found") return jsonify({"error": "Invalid email/username or password"}), 401 if not user.check_password(password): print("Login failed: Incorrect password") return jsonify({"error": "Invalid email/username or password"}), 401 # Generate JWT access token secret_key = os.getenv('SECRET_KEY') if not secret_key: print("ERROR: SECRET_KEY not configured in .env") return jsonify({"error": "Server configuration error"}), 500 try: token = jwt.encode({ 'user_id': user.id, 'username': user.username, 'email': user.email, 'exp': datetime.utcnow() + timedelta(days=7) }, secret_key, algorithm='HS256') except Exception as e: print(f"JWT generation error: {e}") return jsonify({"error": "Token generation failed"}), 500 print(f"✓ Login successful for user: {user.username}") print(f"✓ Token generated successfully\n") # Return successful response return jsonify({ 'message': 'Login successful', 'accessToken': token, 'user': { 'id': user.id, 'user_id': user.user_id, 'username': user.username, 'email': user.email, 'contact': user.contact, 'roles': user.roles, 'user_status': user.user_status, 'photo': user.photo, 'company_logo': user.company_logo } }), 200 except Exception as e: print(f"\n{'='*60}") print(f"LOGIN ERROR") print(f"{'='*60}") print(f"Error type: {type(e).__name__}") print(f"Error message: {str(e)}") import traceback print("\nFull traceback:") traceback.print_exc() print(f"{'='*60}\n") return jsonify({"error": f"Server error: {str(e)}"}), 500 @bp.route('/change-password', methods=['POST']) def change_password(): """ Change user password endpoint Requires current password for verification """ try: data = request.json user_id = data.get('user_id') current_password = data.get('current_password') new_password = data.get('new_password') if not all([user_id, current_password, new_password]): return jsonify({"error": "All fields required"}), 400 user = User.query.get(user_id) if not user: return jsonify({"error": "User not found"}), 404 # Verify current password if not user.check_password(current_password): return jsonify({"error": "Current password is incorrect"}), 401 # Validate new password if len(new_password) < 6: return jsonify({"error": "New password must be at least 6 characters"}), 400 # Set new password (automatically hashed) user.set_password(new_password) user.updated_at = datetime.utcnow() from app import db db.session.commit() print(f"Password changed successfully for user: {user.username}") return jsonify({ 'message': 'Password changed successfully' }), 200 except Exception as e: print(f"Password change error: {e}") return jsonify({"error": str(e)}), 500 def get_current_user_from_token(): """Extract user from Authorization header token""" token = None if 'Authorization' in request.headers: auth_header = request.headers['Authorization'] try: token = auth_header.split(" ")[1] # Bearer except IndexError: return None if not token: return None try: secret_key = os.getenv('SECRET_KEY') data = jwt.decode(token, secret_key, algorithms=['HS256']) current_user = User.query.get(data['user_id']) return current_user except: return None from flask import jsonify, request from sqlalchemy import func, extract from datetime import datetime @bp.route('/dashboard-metrics', methods=['GET']) def get_dashboard_metrics_role_based(): """Get dashboard metrics filtered by user role""" try: # Get current user from token current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 user_role = current_user.roles filter_type = request.args.get('machine_filter', 'all') print(f"\n{'='*60}") print(f"DASHBOARD METRICS REQUEST") print(f"{'='*60}") print(f"User: {current_user.username}") print(f"Role: {user_role}") print(f"User ID: {current_user.id}") print(f"Filter: {filter_type}") print(f"{'='*60}\n") # ROLE-BASED FILTERING if user_role in ['Management', 'SuperAdmin', 'Admin']: response = get_admin_dashboard_metrics(filter_type) elif user_role == 'Client': response = get_Client_dashboard_metrics(current_user.id, filter_type) elif user_role == 'Refiller': response = get_refiller_dashboard_metrics(filter_type) elif user_role == 'Servicer': response = get_servicer_dashboard_metrics(filter_type) else: return jsonify({'error': 'Invalid role'}), 403 return jsonify(response), 200 except Exception as e: import traceback print(f"\n✗ DASHBOARD ERROR") print(f"Error: {str(e)}") traceback.print_exc() return jsonify({'error': f'Internal server error: {str(e)}'}), 500 def get_admin_dashboard_metrics(filter_type): """Get metrics for Management/SuperAdmin/Admin - Overall system stats""" # Machine counts based on filter if filter_type == 'active': machine_count = Machine.query.filter_by(operation_status='active').count() machine_title = 'Active Machines' elif filter_type == 'inactive': machine_count = Machine.query.filter_by(operation_status='inactive').count() machine_title = 'Inactive Machines' else: # 'all' machine_count = Machine.query.count() machine_title = 'All Machines' # Client count clients = User.query.filter_by(roles='Client').count() # Company users company_users = User.query.filter( User.roles.in_(['Management', 'SuperAdmin', 'Admin', 'Refiller', 'Servicer']) ).count() # Active and inactive machines active_machines = Machine.query.filter_by(operation_status='active').count() inactive_machines = Machine.query.filter_by(operation_status='inactive').count() # COUNT TRANSACTIONS AND CALCULATE SALES transactions_count = Transaction.query.count() # Calculate total sales total_sales = db.session.query(func.sum(Transaction.amount))\ .filter(Transaction.status == 'Success')\ .scalar() or 0.0 # PAYMENT BREAKDOWN payment_breakdown = get_payment_breakdown() # MACHINE OPERATION STATUS machine_operation_status = { 'Online': Machine.query.filter_by(connection_status='online').count(), 'Offline': Machine.query.filter_by(connection_status='offline').count(), 'Down (Planned)': Machine.query.filter_by(operation_status='down_planned').count(), 'Down': Machine.query.filter_by(operation_status='down').count(), 'Standby': Machine.query.filter_by(operation_status='standby').count(), 'Terminated': Machine.query.filter_by(operation_status='terminated').count(), 'N/A': Machine.query.filter(Machine.operation_status.is_(None)).count() } # MACHINE STOCK STATUS - Calculate based on vending slots machine_stock_status = calculate_machine_stock_status() # PRODUCT SALES YEARLY product_sales_yearly = get_product_sales_yearly() # SALES YEARLY sales_yearly = get_sales_yearly() # TOP SELLING PRODUCTS top_selling_products = get_top_selling_products() return { 'machine_title': machine_title, 'machine_count': machine_count, 'clients': clients, 'company_users': company_users, 'client_users': clients, 'transactions': transactions_count, 'sales': f'{total_sales:.2f}', 'active_machines': active_machines, 'inactive_machines': inactive_machines, 'payment_breakdown': payment_breakdown, 'machine_operation_status': machine_operation_status, 'machine_stock_status': machine_stock_status, 'product_sales_yearly': product_sales_yearly, 'sales_yearly': sales_yearly, 'top_selling_products': top_selling_products, 'user_role': 'admin', 'user_type': 'Overall System' } def get_Client_dashboard_metrics(user_id, filter_type): """Get metrics for Client - Only their own data""" # Get only machines belonging to this client if filter_type == 'active': machine_count = Machine.query.filter_by( client_id=user_id, operation_status='active' ).count() machine_title = 'My Active Machines' elif filter_type == 'inactive': machine_count = Machine.query.filter_by( client_id=user_id, operation_status='inactive' ).count() machine_title = 'My Inactive Machines' else: # 'all' machine_count = Machine.query.filter_by(client_id=user_id).count() machine_title = 'My Machines' clients = 1 company_users = 0 # Get machine IDs for this client client_machine_ids = [m.machine_id for m in Machine.query.filter_by(client_id=user_id).all()] # Active and inactive machines for this client active_machines = Machine.query.filter_by(client_id=user_id, operation_status='active').count() inactive_machines = Machine.query.filter_by(client_id=user_id, operation_status='inactive').count() # Count transactions for this client's machines transactions_count = Transaction.query.filter( Transaction.machine_id.in_(client_machine_ids) ).count() if client_machine_ids else 0 # Calculate sales for this client's machines total_sales = db.session.query(func.sum(Transaction.amount))\ .filter(Transaction.machine_id.in_(client_machine_ids))\ .filter(Transaction.status == 'Success')\ .scalar() or 0.0 if client_machine_ids else 0.0 # PAYMENT BREAKDOWN for client's machines payment_breakdown = get_payment_breakdown(client_machine_ids) # MACHINE OPERATION STATUS for client's machines machine_operation_status = { 'Online': Machine.query.filter(Machine.client_id == user_id, Machine.connection_status == 'online').count(), 'Offline': Machine.query.filter(Machine.client_id == user_id, Machine.connection_status == 'offline').count(), 'Down (Planned)': Machine.query.filter(Machine.client_id == user_id, Machine.operation_status == 'down_planned').count(), 'Down': Machine.query.filter(Machine.client_id == user_id, Machine.operation_status == 'down').count(), 'Standby': Machine.query.filter(Machine.client_id == user_id, Machine.operation_status == 'standby').count(), 'Terminated': Machine.query.filter(Machine.client_id == user_id, Machine.operation_status == 'terminated').count(), 'N/A': Machine.query.filter(Machine.client_id == user_id, Machine.operation_status.is_(None)).count() } # MACHINE STOCK STATUS for client's machines machine_stock_status = calculate_machine_stock_status(client_machine_ids) # PRODUCT SALES YEARLY for client's machines product_sales_yearly = get_product_sales_yearly(client_machine_ids) # SALES YEARLY for client's machines sales_yearly = get_sales_yearly(client_machine_ids) # TOP SELLING PRODUCTS for client's machines top_selling_products = get_top_selling_products(client_machine_ids) return { 'machine_title': machine_title, 'machine_count': machine_count, 'clients': clients, 'company_users': company_users, 'client_users': clients, 'transactions': transactions_count, 'sales': f'{total_sales:.2f}', 'active_machines': active_machines, 'inactive_machines': inactive_machines, 'payment_breakdown': payment_breakdown, 'machine_operation_status': machine_operation_status, 'machine_stock_status': machine_stock_status, 'product_sales_yearly': product_sales_yearly, 'sales_yearly': sales_yearly, 'top_selling_products': top_selling_products, 'user_role': 'Client', 'user_type': 'My Business' } def get_refiller_dashboard_metrics(filter_type): """Get metrics for Refiller - Product/warehouse focused""" machine_count = Machine.query.count() product_count = Product.query.count() transactions_count = Transaction.query.count() total_sales = db.session.query(func.sum(Transaction.amount))\ .filter(Transaction.status == 'Success')\ .scalar() or 0.0 active_machines = Machine.query.filter_by(operation_status='active').count() inactive_machines = Machine.query.filter_by(operation_status='inactive').count() payment_breakdown = get_payment_breakdown() machine_operation_status = { 'Online': Machine.query.filter_by(connection_status='online').count(), 'Offline': Machine.query.filter_by(connection_status='offline').count(), 'Down (Planned)': Machine.query.filter_by(operation_status='down_planned').count(), 'Down': Machine.query.filter_by(operation_status='down').count(), 'Standby': Machine.query.filter_by(operation_status='standby').count(), 'Terminated': Machine.query.filter_by(operation_status='terminated').count(), 'N/A': Machine.query.filter(Machine.operation_status.is_(None)).count() } machine_stock_status = calculate_machine_stock_status() product_sales_yearly = get_product_sales_yearly() sales_yearly = get_sales_yearly() top_selling_products = get_top_selling_products() return { 'machine_title': 'Machines to Service', 'machine_count': machine_count, 'clients': 0, 'company_users': 0, 'client_users': 0, 'transactions': transactions_count, 'sales': f'{total_sales:.2f}', 'active_machines': active_machines, 'inactive_machines': inactive_machines, 'product_count': product_count, 'payment_breakdown': payment_breakdown, 'machine_operation_status': machine_operation_status, 'machine_stock_status': machine_stock_status, 'product_sales_yearly': product_sales_yearly, 'sales_yearly': sales_yearly, 'top_selling_products': top_selling_products, 'user_role': 'refiller', 'user_type': 'Inventory Management' } def get_servicer_dashboard_metrics(filter_type): """Get metrics for Servicer - Machine maintenance focused""" if filter_type == 'active': machine_count = Machine.query.filter_by(operation_status='active').count() machine_title = 'Active Machines' elif filter_type == 'inactive': machine_count = Machine.query.filter_by(operation_status='inactive').count() machine_title = 'Machines Needing Service' else: machine_count = Machine.query.count() machine_title = 'Total Machines' maintenance_count = Machine.query.filter_by(operation_status='maintenance').count() transactions_count = Transaction.query.count() total_sales = db.session.query(func.sum(Transaction.amount))\ .filter(Transaction.status == 'Success')\ .scalar() or 0.0 active_machines = Machine.query.filter_by(operation_status='active').count() inactive_machines = Machine.query.filter_by(operation_status='inactive').count() payment_breakdown = get_payment_breakdown() machine_operation_status = { 'Online': Machine.query.filter_by(connection_status='online').count(), 'Offline': Machine.query.filter_by(connection_status='offline').count(), 'Down (Planned)': Machine.query.filter_by(operation_status='down_planned').count(), 'Down': Machine.query.filter_by(operation_status='down').count(), 'Standby': Machine.query.filter_by(operation_status='standby').count(), 'Terminated': Machine.query.filter_by(operation_status='terminated').count(), 'N/A': Machine.query.filter(Machine.operation_status.is_(None)).count() } machine_stock_status = calculate_machine_stock_status() product_sales_yearly = get_product_sales_yearly() sales_yearly = get_sales_yearly() top_selling_products = get_top_selling_products() return { 'machine_title': machine_title, 'machine_count': machine_count, 'clients': 0, 'company_users': 0, 'client_users': 0, 'transactions': transactions_count, 'sales': f'{total_sales:.2f}', 'active_machines': active_machines, 'inactive_machines': inactive_machines, 'maintenance_count': maintenance_count, 'payment_breakdown': payment_breakdown, 'machine_operation_status': machine_operation_status, 'machine_stock_status': machine_stock_status, 'product_sales_yearly': product_sales_yearly, 'sales_yearly': sales_yearly, 'top_selling_products': top_selling_products, 'user_role': 'servicer', 'user_type': 'Machine Maintenance' } # HELPER FUNCTIONS def get_payment_breakdown(machine_ids=None): """Calculate payment breakdown from transactions""" query = db.session.query( Transaction.payment_type, func.sum(Transaction.amount).label('total') ) if machine_ids: query = query.filter(Transaction.machine_id.in_(machine_ids)) query = query.filter(Transaction.status == 'Success')\ .group_by(Transaction.payment_type) payment_data = query.all() breakdown = { 'cash': 0.0, 'cashless': 0.0, 'upi_wallet_card': 0.0, 'upi_wallet_paytm': 0.0, 'refund': 0.0, 'refund_processed': 0.0, 'total': 0.0 } for payment_type, total in payment_data: if payment_type and total: payment_type_lower = payment_type.lower() if 'cash' in payment_type_lower and 'cashless' not in payment_type_lower: breakdown['cash'] += float(total) elif 'cashless' in payment_type_lower: breakdown['cashless'] += float(total) elif 'phonepe' in payment_type_lower or 'card' in payment_type_lower: breakdown['upi_wallet_card'] += float(total) elif 'paytm' in payment_type_lower: breakdown['upi_wallet_paytm'] += float(total) # Calculate refunds refund_query = db.session.query(func.sum(Transaction.return_amount)) if machine_ids: refund_query = refund_query.filter(Transaction.machine_id.in_(machine_ids)) breakdown['refund'] = float(refund_query.filter( Transaction.amount_receiving_status == 'Dispense failed refund' ).scalar() or 0.0) breakdown['refund_processed'] = float(refund_query.filter( Transaction.amount_receiving_status == 'Refund processed' ).scalar() or 0.0) breakdown['total'] = sum([ breakdown['cash'], breakdown['cashless'], breakdown['upi_wallet_card'], breakdown['upi_wallet_paytm'] ]) return breakdown def calculate_machine_stock_status(machine_ids=None): """Calculate machine stock status based on vending slots""" query = Machine.query if machine_ids: query = query.filter(Machine.machine_id.in_(machine_ids)) machines = query.all() status_counts = { '75 - 100%': 0, '50 - 75%': 0, '25 - 50%': 0, '0 - 25%': 0, 'N/A': 0 } for machine in machines: slots = VendingSlot.query.filter_by(machine_id=machine.machine_id).all() if not slots: status_counts['N/A'] += 1 continue total_slots = len(slots) filled_slots = sum(1 for slot in slots if slot.units and slot.units > 0) if total_slots == 0: status_counts['N/A'] += 1 else: fill_percentage = (filled_slots / total_slots) * 100 if fill_percentage >= 75: status_counts['75 - 100%'] += 1 elif fill_percentage >= 50: status_counts['50 - 75%'] += 1 elif fill_percentage >= 25: status_counts['25 - 50%'] += 1 elif fill_percentage > 0: status_counts['0 - 25%'] += 1 else: status_counts['0 - 25%'] += 1 return status_counts def get_product_sales_yearly(machine_ids=None): """Get product sales aggregated by year""" query = db.session.query( extract('year', Transaction.created_at).label('year'), func.sum(Transaction.amount).label('total_sales') ) if machine_ids: query = query.filter(Transaction.machine_id.in_(machine_ids)) query = query.filter(Transaction.status == 'Success')\ .group_by(extract('year', Transaction.created_at))\ .order_by('year') results = query.all() return [{'year': str(int(year)), 'amount': float(total_sales)} for year, total_sales in results if year] def get_sales_yearly(machine_ids=None): """Get sales aggregated by year""" return get_product_sales_yearly(machine_ids) def get_top_selling_products(machine_ids=None, limit=10): """Get top selling products""" query = db.session.query( Transaction.product_name, func.sum(Transaction.quantity).label('total_quantity') ) if machine_ids: query = query.filter(Transaction.machine_id.in_(machine_ids)) query = query.filter(Transaction.status == 'Success')\ .group_by(Transaction.product_name)\ .order_by(func.sum(Transaction.quantity).desc())\ .limit(limit) results = query.all() return [{'product_name': product_name, 'quantity': int(total_quantity)} for product_name, total_quantity in results] @bp.route('/product-sales', methods=['GET']) def get_product_sales_filtered(): """Get product sales data filtered by time range""" try: current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 time_range = request.args.get('time_range', 'year') user_role = current_user.roles print(f"Product Sales Filter - User: {current_user.username}, Range: {time_range}") # Get machine IDs based on role machine_ids = None if user_role == 'Client': machine_ids = [m.machine_id for m in Machine.query.filter_by(client_id=current_user.id).all()] # Fetch data based on time range product_sales = get_sales_data_by_range(time_range, machine_ids) return jsonify({'product_sales': product_sales}), 200 except Exception as e: print(f"Error in product sales filter: {str(e)}") return jsonify({'error': str(e)}), 500 @bp.route('/sales-data', methods=['GET']) def get_sales_filtered(): """Get sales data filtered by time range""" try: current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 time_range = request.args.get('time_range', 'year') user_role = current_user.roles print(f"Sales Filter - User: {current_user.username}, Range: {time_range}") # Get machine IDs based on role machine_ids = None if user_role == 'Client': machine_ids = [m.machine_id for m in Machine.query.filter_by(client_id=current_user.id).all()] # Fetch data based on time range sales_data = get_sales_data_by_range(time_range, machine_ids) return jsonify({'sales_data': sales_data}), 200 except Exception as e: print(f"Error in sales filter: {str(e)}") return jsonify({'error': str(e)}), 500 @bp.route('/top-products', methods=['GET']) def get_top_products_filtered(): """Get top selling products filtered by time range""" try: current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 time_range = request.args.get('time_range', 'year') user_role = current_user.roles print(f"Top Products Filter - User: {current_user.username}, Range: {time_range}") # Get machine IDs based on role machine_ids = None if user_role == 'Client': machine_ids = [m.machine_id for m in Machine.query.filter_by(client_id=current_user.id).all()] # Fetch data based on time range top_products = get_top_products_by_range(time_range, machine_ids) return jsonify({'top_products': top_products}), 200 except Exception as e: print(f"Error in top products filter: {str(e)}") return jsonify({'error': str(e)}), 500 # Helper function to get date filter based on time range def get_date_filter(time_range): """Return date filter based on time range""" now = datetime.now() if time_range == 'day': # Last 24 hours return now - timedelta(days=1) elif time_range == 'week': # Last 7 days return now - timedelta(weeks=1) elif time_range == 'month': # Last 30 days return now - timedelta(days=30) elif time_range == 'year': # All time, grouped by year return None else: return None def get_sales_data_by_range(time_range, machine_ids=None): """Get sales data aggregated by time range""" date_filter = get_date_filter(time_range) # Base query query = db.session.query(Transaction.created_at, Transaction.amount)\ .filter(Transaction.status == 'Success') # Apply machine filter if needed if machine_ids: query = query.filter(Transaction.machine_id.in_(machine_ids)) # Apply date filter if date_filter: query = query.filter(Transaction.created_at >= date_filter) transactions = query.all() # Aggregate based on time range if time_range == 'day': # Group by hour aggregated = {} for txn in transactions: hour = txn.created_at.strftime('%H:00') if hour not in aggregated: aggregated[hour] = 0 aggregated[hour] += float(txn.amount) return [{'year': k, 'amount': v} for k, v in sorted(aggregated.items())] elif time_range == 'week': # Group by day aggregated = {} for txn in transactions: day = txn.created_at.strftime('%a') # Mon, Tue, etc. if day not in aggregated: aggregated[day] = 0 aggregated[day] += float(txn.amount) # Sort by weekday order days_order = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] return [{'year': day, 'amount': aggregated.get(day, 0)} for day in days_order] elif time_range == 'month': # Group by day aggregated = {} for txn in transactions: day = txn.created_at.strftime('%d') if day not in aggregated: aggregated[day] = 0 aggregated[day] += float(txn.amount) return [{'year': k, 'amount': v} for k, v in sorted(aggregated.items())] else: # year or all time # Group by year query = db.session.query( extract('year', Transaction.created_at).label('year'), func.sum(Transaction.amount).label('total_sales') ).filter(Transaction.status == 'Success') if machine_ids: query = query.filter(Transaction.machine_id.in_(machine_ids)) query = query.group_by(extract('year', Transaction.created_at))\ .order_by('year') results = query.all() return [{'year': str(int(year)), 'amount': float(total_sales)} for year, total_sales in results if year] def get_top_products_by_range(time_range, machine_ids=None, limit=10): """Get top selling products filtered by time range""" date_filter = get_date_filter(time_range) # Base query query = db.session.query( Transaction.product_name, func.sum(Transaction.quantity).label('total_quantity') ).filter(Transaction.status == 'Success') # Apply machine filter if needed if machine_ids: query = query.filter(Transaction.machine_id.in_(machine_ids)) # Apply date filter if date_filter: query = query.filter(Transaction.created_at >= date_filter) query = query.group_by(Transaction.product_name)\ .order_by(func.sum(Transaction.quantity).desc())\ .limit(limit) results = query.all() return [{'product_name': product_name, 'quantity': int(total_quantity)} for product_name, total_quantity in results] # Additional utility routes for PayU integration @bp.route('/verify-payu-hash', methods=['POST']) def verify_payu_hash(): """Utility endpoint to verify PayU hash calculation""" data = request.json if not data: return jsonify({"error": "No data provided"}), 400 required_fields = ['key', 'txnid', 'amount', 'productinfo', 'firstname', 'email'] for field in required_fields: if field not in data: return jsonify({"error": f"Missing required field: {field}"}), 400 # Get UDF fields (optional) udf1 = data.get('udf1', '') udf2 = data.get('udf2', '') udf3 = data.get('udf3', '') udf4 = data.get('udf4', '') udf5 = data.get('udf5', '') payu_salt = os.getenv('PAYU_MERCHANT_SALT') if not payu_salt: return jsonify({"error": "PAYU_MERCHANT_SALT not configured"}), 500 # Create hash string hash_string = f"{data['key']}|{data['txnid']}|{data['amount']}|{data['productinfo']}|{data['firstname']}|{data['email']}|{udf1}|{udf2}|{udf3}|{udf4}|{udf5}||||||{payu_salt}" # Generate hash calculated_hash = hashlib.sha512(hash_string.encode()).hexdigest() return jsonify({ "hash_string": hash_string, "calculated_hash": calculated_hash, "hash_length": len(calculated_hash) }), 200 @bp.route('/payu-test', methods=['GET']) def payu_test(): """Test endpoint to verify PayU configuration""" payu_key = os.getenv('PAYU_MERCHANT_KEY') payu_salt = os.getenv('PAYU_MERCHANT_SALT') success_url = os.getenv('PAYU_SUCCESS_URL') failure_url = os.getenv('PAYU_FAILURE_URL') return jsonify({ "payu_key": payu_key[:10] + "..." if payu_key else "Not configured", "payu_salt": payu_salt[:10] + "..." if payu_salt else "Not configured", "success_url": success_url or "Not configured", "failure_url": failure_url or "Not configured", "configuration_status": "OK" if all([payu_key, payu_salt]) else "Incomplete" }), 200 @bp.route('/payment-success', methods=['GET', 'POST']) def payment_success_redirect(): """Redirect successful payments to Android app""" # Get query parameters from PayU txnid = request.args.get('txnid') or request.form.get('txnid') status = request.args.get('status') or request.form.get('status') amount = request.args.get('amount') or request.form.get('amount') # ... get other parameters # Create redirect URL to your Android app redirect_url = f"vendingmachine://payment/success?txnid={txnid}&status={status}&amount={amount}" # Return HTML that automatically redirects return f''' Payment Success

Payment Successful!

Redirecting to app...

''' @bp.route('/payment-failure', methods=['GET', 'POST']) def payment_failure_redirect(): """Redirect failed payments to Android app""" # Get query parameters from PayU txnid = request.args.get('txnid') or request.form.get('txnid') status = request.args.get('status') or request.form.get('status') error = request.args.get('error') or request.form.get('error') # Create redirect URL to your Android app redirect_url = f"vendingmachine://payment/failure?txnid={txnid}&status={status}&error={error}" return f''' Payment Failed

Payment Failed

Redirecting to app...

''' @bp.route('/machine/connect', methods=['POST']) def connect_machine(): """Connect to vending machine hardware""" data = request.json or {} port = data.get('port', '/dev/ttyUSB0') # Default Linux USB port baudrate = data.get('baudrate', 9600) try: success = serial_service.connect_to_machine(port, baudrate) if success: return jsonify({ 'success': True, 'message': f'Connected to machine on {port}', 'port': port, 'baudrate': baudrate }), 200 else: return jsonify({ 'success': False, 'error': 'Failed to connect to machine', 'message': 'Check if machine is connected and port is correct' }), 400 except Exception as e: return jsonify({ 'success': False, 'error': str(e), 'message': 'Connection error' }), 500 @bp.route('/machine/status', methods=['GET']) def get_machine_status(): """Check machine hardware status""" try: status = serial_service.check_machine_status() return jsonify(status), 200 except Exception as e: return jsonify({ 'status': 'error', 'error': str(e), 'message': 'Failed to check machine status' }), 500 @bp.route('/machine/dispense', methods=['POST']) def dispense_product(): """Dispense product from specific slot""" data = request.json if not data or 'slotId' not in data: return jsonify({'error': 'Missing slotId parameter'}), 400 slot_id = data.get('slotId') machine_id = data.get('machineId') print(f"=== DISPENSING REQUEST ===") print(f"Machine ID: {machine_id}") print(f"Slot ID: {slot_id}") print("==========================") try: # Send command to actual hardware result = serial_service.send_dispense_command(slot_id) print(f"Dispensing result: {result}") if result['success']: return jsonify({ 'success': True, 'message': result['message'], 'slot_id': slot_id, 'hardware_response': result['response'] }), 200 else: return jsonify({ 'success': False, 'error': result.get('error', 'Dispensing failed'), 'message': result['message'], 'slot_id': slot_id, 'hardware_response': result['response'] }), 400 except Exception as e: print(f"Dispensing error: {e}") return jsonify({ 'success': False, 'error': str(e), 'message': 'System error during dispensing' }), 500 @bp.route('/machine/dispense-multiple', methods=['POST']) def dispense_multiple_products(): """Dispense multiple products sequentially""" data = request.json if not data or 'items' not in data: return jsonify({'error': 'Missing items parameter'}), 400 items = data.get('items', []) machine_id = data.get('machineId') if not items: return jsonify({'error': 'No items to dispense'}), 400 print(f"=== MULTIPLE DISPENSING REQUEST ===") print(f"Machine ID: {machine_id}") print(f"Items count: {len(items)}") results = [] all_successful = True try: for item in items: slot_id = item.get('slotId') quantity = item.get('quantity', 1) product_name = item.get('productName', 'Unknown') print(f"Dispensing {quantity}x {product_name} from slot {slot_id}") # Dispense each quantity one by one for i in range(quantity): result = serial_service.send_dispense_command(slot_id) results.append({ 'slot_id': slot_id, 'product_name': product_name, 'attempt': i + 1, 'total_quantity': quantity, 'success': result['success'], 'response': result['response'], 'message': result['message'] }) if not result['success']: all_successful = False print(f"Failed to dispense item {i+1} from slot {slot_id}: {result['message']}") break # Stop dispensing this item if one fails else: print(f"Successfully dispensed item {i+1} from slot {slot_id}") # Small delay between dispensing items if i < quantity - 1: time.sleep(0.5) return jsonify({ 'success': all_successful, 'message': 'All products dispensed successfully' if all_successful else 'Some products failed to dispense', 'results': results, 'total_items': len(items), 'successful_dispensing': sum(1 for r in results if r['success']) }), 200 if all_successful else 207 # 207 = Multi-Status except Exception as e: print(f"Multiple dispensing error: {e}") return jsonify({ 'success': False, 'error': str(e), 'message': 'System error during multiple dispensing', 'results': results }), 500 @bp.route('/machine/reset', methods=['POST']) def reset_machine(): """Reset machine to clear error states""" try: result = serial_service.reset_machine() if result['success']: return jsonify({ 'success': True, 'message': 'Machine reset successfully', 'response': result.get('response', '') }), 200 else: return jsonify({ 'success': False, 'error': result.get('error', 'Reset failed'), 'message': 'Failed to reset machine' }), 400 except Exception as e: return jsonify({ 'success': False, 'error': str(e), 'message': 'Error during machine reset' }), 500 @bp.route('/machine/disconnect', methods=['POST']) def disconnect_machine(): """Disconnect from machine hardware""" try: serial_service.disconnect() return jsonify({ 'success': True, 'message': 'Disconnected from machine successfully' }), 200 except Exception as e: return jsonify({ 'success': False, 'error': str(e), 'message': 'Error during disconnection' }), 500 # Update the existing payment success route to trigger dispensing @bp.route('/payment-confirmed', methods=['POST']) def payment_confirmed(): """Handle payment confirmation and trigger dispensing""" data = request.json machine_id = data.get('machineId') cart_items = data.get('cartItems', []) payment_type = data.get('paymentType', 'wallet') total_amount = data.get('totalAmount', 0) if not machine_id or not cart_items: return jsonify({'error': 'Missing machineId or cartItems'}), 400 transaction_records = [] try: # Create transaction records for each item for item in cart_items: transaction = TransactionService.create_transaction( machine_id=machine_id, product_name=item.get('productName', 'Unknown'), quantity=item.get('quantity', 1), amount=float(item.get('price', 0)) * int(item.get('quantity', 1)), payment_type=payment_type, status='Unprocessed' ) transaction_records.append(transaction) # Attempt dispensing dispensing_items = [] for item in cart_items: dispensing_items.append({ 'slotId': item.get('slotId'), 'quantity': item.get('quantity', 1), 'productName': item.get('productName', 'Unknown') }) # Call dispensing endpoint dispense_result = serial_service.send_dispense_command(dispensing_items[0]['slotId']) # Update transaction statuses based on dispensing result for transaction in transaction_records: if dispense_result['success']: TransactionService.update_transaction_status( transaction.transaction_id, status='Success' ) else: TransactionService.update_transaction_status( transaction.transaction_id, status='Dispense failed', amount_receiving_status='Pending refund', return_amount=transaction.amount ) return jsonify({ 'success': dispense_result['success'], 'message': 'Payment processed', 'transactions': [t.to_dict() for t in transaction_records], 'dispensing_result': dispense_result }), 200 except Exception as e: import traceback traceback.print_exc() return jsonify({'error': str(e)}), 500 @bp.route('/transactions', methods=['GET']) def get_transactions(): """Get all transactions with optional filters""" try: # Get current user from token current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 # Build filters from query params filters = {} # Role-based filtering if current_user.roles == 'Client': # Client can only see their own machines' transactions machine_ids = [m.machine_id for m in Machine.query.filter_by(client_id=current_user.id).all()] transactions = Transaction.query.filter(Transaction.machine_id.in_(machine_ids)).order_by(Transaction.created_at.desc()).all() else: # Admin/Management can see all if request.args.get('machine_id'): filters['machine_id'] = request.args.get('machine_id') if request.args.get('status'): filters['status'] = request.args.get('status') transactions = TransactionService.get_all_transactions(filters) return jsonify([t.to_dict() for t in transactions]), 200 except Exception as e: import traceback traceback.print_exc() return jsonify({'error': str(e)}), 500 @bp.route('/transactions/', methods=['GET']) def get_transaction(transaction_id): """Get single transaction details""" try: transaction = TransactionService.get_transaction_by_id(transaction_id) if not transaction: return jsonify({'error': 'Transaction not found'}), 404 return jsonify(transaction.to_dict()), 200 except Exception as e: return jsonify({'error': str(e)}), 500 @bp.route('/transactions//refund', methods=['POST']) def refund_transaction(transaction_id): """Process refund for failed dispensing""" try: transaction = TransactionService.get_transaction_by_id(transaction_id) if not transaction: return jsonify({'error': 'Transaction not found'}), 404 # Update transaction with refund info updated = TransactionService.update_transaction_status( transaction_id, status='Dispense failed', amount_receiving_status='Dispense failed refund', return_amount=transaction.amount ) # TODO: Integrate with actual payment gateway refund API return jsonify({ 'message': 'Refund processed successfully', 'transaction': updated.to_dict() }), 200 except Exception as e: return jsonify({'error': str(e)}), 500 @bp.route('/transactions/bulk-create', methods=['POST']) def bulk_create_transactions(): """Create multiple transactions at once""" try: print("\n" + "=" * 60) print("BULK TRANSACTION CREATION") print("=" * 60) data = request.json transactions_data = data.get('transactions', []) if not transactions_data: return jsonify({'error': 'No transactions provided'}), 400 print(f"Creating {len(transactions_data)} transactions") created_transactions = [] for trans_data in transactions_data: machine_id = trans_data.get('machine_id') product_name = trans_data.get('product_name') quantity = trans_data.get('quantity') amount = trans_data.get('amount') payment_type = trans_data.get('payment_type', 'cash') status = trans_data.get('status', 'Success') print(f" - {product_name} x {quantity} = ₹{amount} (Machine: {machine_id})") transaction = TransactionService.create_transaction( machine_id=machine_id, product_name=product_name, quantity=quantity, amount=amount, payment_type=payment_type, status=status ) created_transactions.append(transaction.to_dict()) print(f"✓ {len(created_transactions)} transactions created successfully") print("=" * 60 + "\n") return jsonify({ 'message': 'Transactions created successfully', 'transactions': created_transactions, 'count': len(created_transactions) }), 201 except Exception as e: print(f"\n✗ TRANSACTION CREATION ERROR") print(f"Error: {str(e)}") import traceback traceback.print_exc() print("=" * 60 + "\n") return jsonify({'error': str(e)}), 500 # Role Management Endpoints @bp.route('/roles', methods=['GET']) def get_roles(): """Get all roles""" try: current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 # Only Management and SuperAdmin can view roles if current_user.roles not in ['Management', 'SuperAdmin','Admin']: return jsonify({'error': 'Permission denied'}), 403 roles = RoleService.get_all_roles() return jsonify([role.to_dict() for role in roles]), 200 except Exception as e: return jsonify({'error': str(e)}), 500 @bp.route('/roles', methods=['POST']) def create_role(): """Create new role""" # Get current user current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 # Only Management and SuperAdmin can create roles if current_user.roles not in ['Management', 'SuperAdmin', 'Admin']: return jsonify({'error': 'Permission denied'}), 403 try: data = request.json # ADD CREATED_BY data['created_by'] = current_user.id new_role = RoleService.create_role(data) print(f"✓ Role created by: {current_user.username}") return jsonify(new_role.to_dict()), 201 except ValueError as e: return jsonify({'error': str(e)}), 400 except Exception as e: return jsonify({'error': str(e)}), 500 @bp.route('/roles/', methods=['PUT']) def update_role(role_id): """Update existing role""" try: current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 # Only Management and SuperAdmin can update roles if current_user.roles not in ['Management', 'SuperAdmin','Admin']: return jsonify({'error': 'Permission denied'}), 403 data = request.json updated_role = RoleService.update_role(role_id, data) return jsonify(updated_role.to_dict()), 200 except ValueError as e: return jsonify({'error': str(e)}), 400 except Exception as e: return jsonify({'error': str(e)}), 500 @bp.route('/roles/', methods=['DELETE']) def delete_role(role_id): """Delete role""" try: current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 # Only Management and SuperAdmin can delete roles if current_user.roles not in ['Management', 'SuperAdmin','Admin']: return jsonify({'error': 'Permission denied'}), 403 RoleService.delete_role(role_id) return jsonify({'message': 'Role deleted successfully'}), 200 except ValueError as e: return jsonify({'error': str(e)}), 400 except Exception as e: return jsonify({'error': str(e)}), 500 @bp.route('/permissions', methods=['GET']) def get_available_permissions(): """Get all available permissions""" try: current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 # Only Management and SuperAdmin can view permissions if current_user.roles not in ['Management', 'SuperAdmin','Admin']: return jsonify({'error': 'Permission denied'}), 403 # Define all available permissions permissions = [ {'id': 'view_dashboard', 'name': 'View Dashboard', 'description': 'Access to dashboard', 'module': 'Dashboard'}, {'id': 'view_users', 'name': 'View Users', 'description': 'Can view user listings', 'module': 'Users'}, {'id': 'create_users', 'name': 'Create Users', 'description': 'Can create new users', 'module': 'Users'}, {'id': 'edit_users', 'name': 'Edit Users', 'description': 'Can modify users', 'module': 'Users'}, {'id': 'delete_users', 'name': 'Delete Users', 'description': 'Can remove users', 'module': 'Users'}, {'id': 'view_machines', 'name': 'View Machines', 'description': 'Can view machines', 'module': 'Machines'}, {'id': 'create_machines', 'name': 'Create Machines', 'description': 'Can add new machines', 'module': 'Machines'}, {'id': 'edit_machines', 'name': 'Edit Machines', 'description': 'Can modify machines', 'module': 'Machines'}, {'id': 'delete_machines', 'name': 'Delete Machines', 'description': 'Can remove machines', 'module': 'Machines'}, {'id': 'view_products', 'name': 'View Products', 'description': 'Can view products', 'module': 'Products'}, {'id': 'create_products', 'name': 'Create Products', 'description': 'Can add products', 'module': 'Products'}, {'id': 'edit_products', 'name': 'Edit Products', 'description': 'Can modify products', 'module': 'Products'}, {'id': 'delete_products', 'name': 'Delete Products', 'description': 'Can remove products', 'module': 'Products'}, {'id': 'view_transactions', 'name': 'View Transactions', 'description': 'Can view transactions', 'module': 'Transactions'}, {'id': 'view_reports', 'name': 'View Reports', 'description': 'Can access reports', 'module': 'Reports'}, {'id': 'export_data', 'name': 'Export Data', 'description': 'Can export data', 'module': 'Reports'}, {'id': 'view_roles', 'name': 'View Roles', 'description': 'Can view role listings', 'module': 'Roles'}, {'id': 'create_roles', 'name': 'Create Roles', 'description': 'Can create new roles', 'module': 'Roles'}, {'id': 'edit_roles', 'name': 'Edit Roles', 'description': 'Can modify roles', 'module': 'Roles'}, {'id': 'delete_roles', 'name': 'Delete Roles', 'description': 'Can remove roles', 'module': 'Roles'}, {'id': 'system_settings', 'name': 'System Settings', 'description': 'Can modify system configuration', 'module': 'Administration'} ] return jsonify(permissions), 200 except Exception as e: return jsonify({'error': str(e)}), 500 @bp.route('/branches', methods=['GET']) def get_branches(): """Get all branches""" try: # Get current user from token current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 from app.services.services import BranchService branches = BranchService.get_all_branches() return jsonify([branch.to_dict() for branch in branches]), 200 except Exception as e: import traceback traceback.print_exc() return jsonify({'error': str(e)}), 500 @bp.route('/branches/', methods=['GET']) def get_branch(branch_id): """Get single branch by ID""" try: current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 from app.services.services import BranchService branch = BranchService.get_branch_by_id(branch_id) return jsonify(branch.to_dict()), 200 except Exception as e: return jsonify({'error': str(e)}), 404 @bp.route('/branches', methods=['POST']) def create_branch(): print("\n" + "=" * 60) print("CREATE BRANCH REQUEST") print("=" * 60) # Get current user current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 # Only Admin, Management, and SuperAdmin can create branches if current_user.roles not in ['Admin', 'Management', 'SuperAdmin']: return jsonify({'error': 'Permission denied'}), 403 try: data = request.json # ADD CREATED_BY data['created_by'] = current_user.id print(f"User: {current_user.username} ({current_user.roles})") print(f"Data: {data}") new_branch = BranchService.create_branch(data) print(f"✓ Branch created successfully by: {current_user.username}") print("=" * 60 + "\n") return jsonify(new_branch.to_dict()), 201 except ValueError as e: print(f"\n✗ Validation Error: {str(e)}") print("=" * 60 + "\n") return jsonify({'error': str(e)}), 400 except Exception as e: print(f"\n✗ SERVER ERROR") print(f"Error type: {type(e).__name__}") print(f"Error message: {str(e)}") import traceback print("\nFull traceback:") traceback.print_exc() print("=" * 60 + "\n") return jsonify({'error': f'Server error: {str(e)}'}), 500 @bp.route('/branches/', methods=['PUT']) def update_branch(branch_id): """Update existing branch""" print("\n" + "=" * 60) print(f"UPDATE BRANCH REQUEST - ID: {branch_id}") print("=" * 60) try: # Get current user from token current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 # Only Admin, Management, and SuperAdmin can update branches if current_user.roles not in ['Admin', 'Management', 'SuperAdmin']: return jsonify({'error': 'Permission denied'}), 403 data = request.json print(f"User: {current_user.username} ({current_user.roles})") print(f"Data: {data}") from app.services.services import BranchService updated_branch = BranchService.update_branch(branch_id, data) print(f"✓ Branch updated successfully: {updated_branch.branch_id}") print("=" * 60 + "\n") return jsonify(updated_branch.to_dict()), 200 except ValueError as e: print(f"\n✗ Validation Error: {str(e)}") print("=" * 60 + "\n") return jsonify({'error': str(e)}), 400 except Exception as e: print(f"\n✗ SERVER ERROR") print(f"Error type: {type(e).__name__}") print(f"Error message: {str(e)}") import traceback print("\nFull traceback:") traceback.print_exc() print("=" * 60 + "\n") return jsonify({'error': f'Server error: {str(e)}'}), 500 @bp.route('/branches/', methods=['DELETE']) def delete_branch(branch_id): """Delete branch""" print("\n" + "=" * 60) print(f"DELETE BRANCH REQUEST - ID: {branch_id}") print("=" * 60) try: # Get current user from token current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 # Only Admin, Management, and SuperAdmin can delete branches if current_user.roles not in ['Admin', 'Management', 'SuperAdmin']: return jsonify({'error': 'Permission denied'}), 403 print(f"User: {current_user.username} ({current_user.roles})") from app.services.services import BranchService BranchService.delete_branch(branch_id) print(f"✓ Branch deleted successfully") print("=" * 60 + "\n") return jsonify({'message': 'Branch deleted successfully'}), 200 except Exception as e: print(f"\n✗ ERROR: {str(e)}") print("=" * 60 + "\n") return jsonify({'error': str(e)}), 500 @bp.route('/branches/search', methods=['GET']) def search_branches(): """Search branches by query""" try: current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 query = request.args.get('q', '') if not query: return jsonify({'error': 'Search query required'}), 400 from app.services.services import BranchService branches = BranchService.search_branches(query) return jsonify([branch.to_dict() for branch in branches]), 200 except Exception as e: return jsonify({'error': str(e)}), 500 # Brand Management Routes @bp.route('/brands', methods=['GET']) def get_brands(): """Get all brands""" try: current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 from app.services.services import BrandService brands = BrandService.get_all_brands() return jsonify([brand.to_dict() for brand in brands]), 200 except Exception as e: import traceback traceback.print_exc() return jsonify({'error': str(e)}), 500 @bp.route('/brands/', methods=['GET']) def get_brand(brand_id): """Get single brand by ID""" try: current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 from app.services.services import BrandService brand = BrandService.get_brand_by_id(brand_id) return jsonify(brand.to_dict()), 200 except Exception as e: return jsonify({'error': str(e)}), 404 @bp.route('/brands', methods=['POST']) def create_brand(): print("\n" + "=" * 60) print("CREATE BRAND REQUEST") print("=" * 60) # Get current user current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 # Only Admin, Management, and SuperAdmin can create brands if current_user.roles not in ['Admin', 'Management', 'SuperAdmin']: return jsonify({'error': 'Permission denied'}), 403 try: # Get form data data = request.form.to_dict() # ADD CREATED_BY data['created_by'] = current_user.id # Get image file image_file = request.files.get('image') print(f"User: {current_user.username} ({current_user.roles})") print(f"Data: {data}") print(f"Image: {image_file.filename if image_file else 'None'}") new_brand = BrandService.create_brand(data, image_file) print(f"✓ Brand created successfully by: {current_user.username}") print("=" * 60 + "\n") return jsonify(new_brand.to_dict()), 201 except ValueError as e: print(f"\n✗ Validation Error: {str(e)}") print("=" * 60 + "\n") return jsonify({'error': str(e)}), 400 except Exception as e: print(f"\n✗ SERVER ERROR") print(f"Error type: {type(e).__name__}") print(f"Error message: {str(e)}") import traceback print("\nFull traceback:") traceback.print_exc() print("=" * 60 + "\n") return jsonify({'error': f'Server error: {str(e)}'}), 500 @bp.route('/brands/', methods=['PUT']) def update_brand(brand_id): """Update existing brand""" print("\n" + "=" * 60) print(f"UPDATE BRAND REQUEST - ID: {brand_id}") print("=" * 60) try: current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 # Only Admin, Management, and SuperAdmin can update brands if current_user.roles not in ['Admin', 'Management', 'SuperAdmin']: return jsonify({'error': 'Permission denied'}), 403 # Get form data data = request.form.to_dict() # Get image file if provided image_file = request.files.get('image') if 'image' in request.files else None print(f"User: {current_user.username} ({current_user.roles})") print(f"Data: {data}") print(f"Image: {image_file.filename if image_file else 'No change'}") from app.services.services import BrandService updated_brand = BrandService.update_brand(brand_id, data, image_file) print(f"✓ Brand updated successfully: {updated_brand.brand_id}") print("=" * 60 + "\n") return jsonify(updated_brand.to_dict()), 200 except ValueError as e: print(f"\n✗ Validation Error: {str(e)}") print("=" * 60 + "\n") return jsonify({'error': str(e)}), 400 except Exception as e: print(f"\n✗ SERVER ERROR") print(f"Error type: {type(e).__name__}") print(f"Error message: {str(e)}") import traceback print("\nFull traceback:") traceback.print_exc() print("=" * 60 + "\n") return jsonify({'error': f'Server error: {str(e)}'}), 500 @bp.route('/brands/', methods=['DELETE']) def delete_brand(brand_id): """Delete brand""" print("\n" + "=" * 60) print(f"DELETE BRAND REQUEST - ID: {brand_id}") print("=" * 60) try: current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 # Only Admin, Management, and SuperAdmin can delete brands if current_user.roles not in ['Admin', 'Management', 'SuperAdmin']: return jsonify({'error': 'Permission denied'}), 403 print(f"User: {current_user.username} ({current_user.roles})") from app.services.services import BrandService BrandService.delete_brand(brand_id) print(f"✓ Brand deleted successfully") print("=" * 60 + "\n") return jsonify({'message': 'Brand deleted successfully'}), 200 except Exception as e: print(f"\n✗ ERROR: {str(e)}") print("=" * 60 + "\n") return jsonify({'error': str(e)}), 500 @bp.route('/brands/search', methods=['GET']) def search_brands(): """Search brands by query""" try: current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 query = request.args.get('q', '') if not query: return jsonify({'error': 'Search query required'}), 400 from app.services.services import BrandService brands = BrandService.search_brands(query) return jsonify([brand.to_dict() for brand in brands]), 200 except Exception as e: return jsonify({'error': str(e)}), 500 @bp.route('/brands/by-branch/', methods=['GET']) def get_brands_by_branch(branch_id): """Get all brands for a specific branch""" try: current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 from app.services.services import BrandService brands = BrandService.get_brands_by_branch(branch_id) return jsonify([brand.to_dict() for brand in brands]), 200 except Exception as e: return jsonify({'error': str(e)}), 500 # Serve brand images @bp.route('/uploads/brand_images/') def serve_brand_image(filename): return send_from_directory(os.path.join(bp.root_path, '..', 'Uploads', 'brand_images'), filename) # Category Management Routes @bp.route('/categories', methods=['GET']) def get_categories(): """Get all categories""" try: current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 from app.services.services import CategoryService categories = CategoryService.get_all_categories() return jsonify([category.to_dict() for category in categories]), 200 except Exception as e: import traceback traceback.print_exc() return jsonify({'error': str(e)}), 500 @bp.route('/categories/', methods=['GET']) def get_category(category_id): """Get single category by ID""" try: current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 from app.services.services import CategoryService category = CategoryService.get_category_by_id(category_id) return jsonify(category.to_dict()), 200 except Exception as e: return jsonify({'error': str(e)}), 404 @bp.route('/categories', methods=['POST']) def create_category(): print("\n" + "=" * 60) print("CREATE CATEGORY REQUEST") print("=" * 60) # Get current user current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 # Only Admin, Management, and SuperAdmin can create categories if current_user.roles not in ['Admin', 'Management', 'SuperAdmin']: return jsonify({'error': 'Permission denied'}), 403 try: # Get form data data = request.form.to_dict() # ADD CREATED_BY data['created_by'] = current_user.id # Get image file image_file = request.files.get('image') print(f"User: {current_user.username} ({current_user.roles})") print(f"Data: {data}") new_category = CategoryService.create_category(data, image_file) print(f"✓ Category created successfully by: {current_user.username}") print("=" * 60 + "\n") return jsonify(new_category.to_dict()), 201 except ValueError as e: print(f"\n✗ Validation Error: {str(e)}") print("=" * 60 + "\n") return jsonify({'error': str(e)}), 400 except Exception as e: print(f"\n✗ SERVER ERROR") print(f"Error type: {type(e).__name__}") print(f"Error message: {str(e)}") import traceback print("\nFull traceback:") traceback.print_exc() print("=" * 60 + "\n") return jsonify({'error': f'Server error: {str(e)}'}), 500 @bp.route('/categories/', methods=['PUT']) def update_category(category_id): """Update existing category""" print("\n" + "=" * 60) print(f"UPDATE CATEGORY REQUEST - ID: {category_id}") print("=" * 60) try: current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 # Only Admin, Management, and SuperAdmin can update categories if current_user.roles not in ['Admin', 'Management', 'SuperAdmin']: return jsonify({'error': 'Permission denied'}), 403 # Get form data data = request.form.to_dict() # Get image file if provided image_file = request.files.get('image') if 'image' in request.files else None print(f"User: {current_user.username} ({current_user.roles})") print(f"Data: {data}") print(f"Image: {image_file.filename if image_file else 'No change'}") from app.services.services import CategoryService updated_category = CategoryService.update_category(category_id, data, image_file) print(f"✓ Category updated successfully: {updated_category.category_id}") print("=" * 60 + "\n") return jsonify(updated_category.to_dict()), 200 except ValueError as e: print(f"\n✗ Validation Error: {str(e)}") print("=" * 60 + "\n") return jsonify({'error': str(e)}), 400 except Exception as e: print(f"\n✗ SERVER ERROR") print(f"Error type: {type(e).__name__}") print(f"Error message: {str(e)}") import traceback print("\nFull traceback:") traceback.print_exc() print("=" * 60 + "\n") return jsonify({'error': f'Server error: {str(e)}'}), 500 @bp.route('/categories/', methods=['DELETE']) def delete_category(category_id): """Delete category""" print("\n" + "=" * 60) print(f"DELETE CATEGORY REQUEST - ID: {category_id}") print("=" * 60) try: current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 # Only Admin, Management, and SuperAdmin can delete categories if current_user.roles not in ['Admin', 'Management', 'SuperAdmin']: return jsonify({'error': 'Permission denied'}), 403 print(f"User: {current_user.username} ({current_user.roles})") from app.services.services import CategoryService CategoryService.delete_category(category_id) print(f"✓ Category deleted successfully") print("=" * 60 + "\n") return jsonify({'message': 'Category deleted successfully'}), 200 except Exception as e: print(f"\n✗ ERROR: {str(e)}") print("=" * 60 + "\n") return jsonify({'error': str(e)}), 500 @bp.route('/categories/search', methods=['GET']) def search_categories(): """Search categories by query""" try: current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 query = request.args.get('q', '') if not query: return jsonify({'error': 'Search query required'}), 400 from app.services.services import CategoryService categories = CategoryService.search_categories(query) return jsonify([category.to_dict() for category in categories]), 200 except Exception as e: return jsonify({'error': str(e)}), 500 @bp.route('/categories/by-brand/', methods=['GET']) def get_categories_by_brand(brand_id): """Get all categories for a specific brand""" try: current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 from app.services.services import CategoryService categories = CategoryService.get_categories_by_brand(brand_id) return jsonify([category.to_dict() for category in categories]), 200 except Exception as e: return jsonify({'error': str(e)}), 500 @bp.route('/categories/by-branch/', methods=['GET']) def get_categories_by_branch(branch_id): """Get all categories for a specific branch""" try: current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 from app.services.services import CategoryService categories = CategoryService.get_categories_by_branch(branch_id) return jsonify([category.to_dict() for category in categories]), 200 except Exception as e: return jsonify({'error': str(e)}), 500 # Serve category images @bp.route('/uploads/category_images/') def serve_category_image(filename): return send_from_directory(os.path.join(bp.root_path, '..', 'Uploads', 'category_images'), filename) @bp.route('/subcategories', methods=['GET']) def get_subcategories(): try: current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 from app.services.services import SubCategoryService return jsonify([s.to_dict() for s in SubCategoryService.get_all()]), 200 except Exception as e: return jsonify({'error': str(e)}), 500 @bp.route('/subcategories/', methods=['GET']) def get_subcategory(id): try: current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 from app.services.services import SubCategoryService return jsonify(SubCategoryService.get_by_id(id).to_dict()), 200 except Exception as e: return jsonify({'error': str(e)}), 404 @bp.route('/subcategories', methods=['POST']) def create_subcategory(): # Get current user current_user = get_current_user_from_token() if not current_user: return jsonify({'error': 'Authentication required'}), 401 if current_user.roles not in ['Admin', 'Management', 'SuperAdmin']: return jsonify({'error': 'Permission denied'}), 403 try: data = request.form.to_dict() # ADD CREATED_BY data['created_by'] = current_user.id image = request.files.get('image') new_sub = SubCategoryService.create(data, image) print(f"✓ SubCategory created by: {current_user.username}") return jsonify(new_sub.to_dict()), 201 except ValueError as e: return jsonify({'error': str(e)}), 400 except Exception as e: import traceback traceback.print_exc() return jsonify({'error': str(e)}), 500 @bp.route('/subcategories/', methods=['PUT']) def update_subcategory(id): try: current_user = get_current_user_from_token() if not current_user or current_user.roles not in ['Admin', 'Management', 'SuperAdmin']: return jsonify({'error': 'Permission denied'}), 403 data = request.form.to_dict() image = request.files.get('image') if 'image' in request.files else None from app.services.services import SubCategoryService updated = SubCategoryService.update(id, data, image) return jsonify(updated.to_dict()), 200 except ValueError as e: return jsonify({'error': str(e)}), 400 except Exception as e: return jsonify({'error': str(e)}), 500 @bp.route('/subcategories/', methods=['DELETE']) def delete_subcategory(id): try: current_user = get_current_user_from_token() if not current_user or current_user.roles not in ['Admin', 'Management', 'SuperAdmin']: return jsonify({'error': 'Permission denied'}), 403 from app.services.services import SubCategoryService SubCategoryService.delete(id) return jsonify({'message': 'SubCategory deleted'}), 200 except Exception as e: return jsonify({'error': str(e)}), 500 @bp.route('/uploads/subcategory_images/') def serve_subcategory_image(filename): return send_from_directory(os.path.join(bp.root_path, '..', 'Uploads', 'subcategory_images'), filename)