Files
IOT_application/Machine-Backend/app/routes/routes.py
2025-10-25 09:35:47 +05:30

2188 lines
79 KiB
Python

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
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/<machine_id>', 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/<filename>')
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']}")
new_machine = MachineService.create_machine(data)
print(f"✓ Machine created: {new_machine.machine_id} for client_id: {new_machine.client_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/<int:id>', 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/<int:id>', 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
# 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)
try:
# Get form data
data = request.form.to_dict()
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")
if document_files:
for i, f in enumerate(document_files):
print(f" {i+1}. {f.filename}")
print(f" Metadata: {documents_metadata}")
# 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("✓ User created successfully!")
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
@bp.route('/users/<int:id>', methods=['PUT'])
def update_user(id):
try:
data = request.form.to_dict()
# 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 - THIS IS NEW
documents_metadata = request.form.get('documents_metadata')
# Pass metadata to service - UPDATED PARAMETER LIST
updated_user = UserService.update_user(
id,
data,
photo_file,
logo_file,
document_files,
documents_metadata # NEW PARAMETER
)
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/<int:id>', 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/<int:user_id>/documents/<path:document_path>', 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/<filename>')
def serve_user_photo(filename):
return send_from_directory(os.path.join(bp.root_path, '..', 'Uploads', 'user_photos'), filename)
@bp.route('/uploads/company_logos/<filename>')
def serve_company_logo(filename):
return send_from_directory(os.path.join(bp.root_path, '..', 'Uploads', 'company_logos'), filename)
@bp.route('/uploads/user_documents/<filename>')
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():
try:
data = request.form
file = request.files.get('product_image')
new_product = ProductService.create_product(data, file)
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/<int:id>', 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/<int:id>', 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/<int:id>', 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/<machine_id>', 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/<machine_id>/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 <token>
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'''
<!DOCTYPE html>
<html>
<head>
<title>Payment Success</title>
<script>
window.location.href = "{redirect_url}";
setTimeout(function() {{
document.getElementById('fallback').style.display = 'block';
}}, 3000);
</script>
</head>
<body>
<div style="text-align: center; padding: 50px;">
<h2>Payment Successful!</h2>
<p>Redirecting to app...</p>
<div id="fallback" style="display: none;">
<p>If app doesn't open automatically:</p>
<a href="{redirect_url}">Open App</a>
</div>
</div>
</body>
</html>
'''
@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'''
<!DOCTYPE html>
<html>
<head>
<title>Payment Failed</title>
<script>
window.location.href = "{redirect_url}";
setTimeout(function() {{
document.getElementById('fallback').style.display = 'block';
}}, 3000);
</script>
</head>
<body>
<div style="text-align: center; padding: 50px;">
<h2>Payment Failed</h2>
<p>Redirecting to app...</p>
<div id="fallback" style="display: none;">
<p>If app doesn't open automatically:</p>
<a href="{redirect_url}">Open App</a>
</div>
</div>
</body>
</html>
'''
@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/<transaction_id>', 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/<transaction_id>/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"""
try:
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
data = request.json
new_role = RoleService.create_role(data)
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/<int:role_id>', 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/<int:role_id>', 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