1724 lines
61 KiB
Python
1724 lines
61 KiB
Python
from app import db # Add this if not already there
|
|
from sqlalchemy import func # 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
|
|
|
|
|
|
@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']:
|
|
# Show overall system stats
|
|
response = get_admin_dashboard_metrics(filter_type)
|
|
elif user_role == 'Client':
|
|
# Show only this client's data
|
|
response = get_Client_dashboard_metrics(current_user.id, filter_type)
|
|
elif user_role == 'Refiller':
|
|
# Show product/warehouse related stats
|
|
response = get_refiller_dashboard_metrics(filter_type)
|
|
elif user_role == 'Servicer':
|
|
# Show machine maintenance stats
|
|
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 (users with role 'Client')
|
|
clients = User.query.filter_by(roles='Client').count()
|
|
|
|
# Company users (Admin, SuperAdmin, Management, Refiller, Servicer)
|
|
company_users = User.query.filter(
|
|
User.roles.in_(['Management', 'SuperAdmin', 'Admin', 'Refiller', 'Servicer'])
|
|
).count()
|
|
|
|
# COUNT TRANSACTIONS AND CALCULATE SALES
|
|
transactions_count = Transaction.query.count()
|
|
|
|
# Calculate total sales (sum of all successful transaction amounts)
|
|
from sqlalchemy import func
|
|
total_sales = db.session.query(func.sum(Transaction.amount))\
|
|
.filter(Transaction.status == 'Success')\
|
|
.scalar() or 0.0
|
|
|
|
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}',
|
|
'user_role': 'admin',
|
|
'user_type': 'Overall System'
|
|
}
|
|
|
|
|
|
def get_Client_dashboard_metrics(user_id, filter_type):
|
|
"""Get metrics for Client/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'
|
|
|
|
# For Client, clients = 1 (themselves)
|
|
clients = 1
|
|
|
|
# Company users not relevant for Client
|
|
company_users = 0
|
|
|
|
# GET THIS CLIENT'S TRANSACTIONS AND SALES
|
|
# Get machine IDs for this client
|
|
client_machine_ids = [m.machine_id for m in Machine.query.filter_by(client_id=user_id).all()]
|
|
|
|
# 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
|
|
from sqlalchemy import func
|
|
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
|
|
|
|
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}',
|
|
'user_role': 'Client',
|
|
'user_type': 'My Business'
|
|
}
|
|
|
|
|
|
def get_refiller_dashboard_metrics(filter_type):
|
|
"""Get metrics for Refiller - Product/warehouse focused"""
|
|
# Machines they need to service
|
|
machine_count = Machine.query.count()
|
|
|
|
# Products in warehouse
|
|
product_count = Product.query.count()
|
|
|
|
# Transaction count (all)
|
|
transactions_count = Transaction.query.count()
|
|
|
|
# Total sales
|
|
from sqlalchemy import func
|
|
total_sales = db.session.query(func.sum(Transaction.amount))\
|
|
.filter(Transaction.status == 'Success')\
|
|
.scalar() or 0.0
|
|
|
|
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}',
|
|
'product_count': product_count,
|
|
'user_role': 'refiller',
|
|
'user_type': 'Inventory Management'
|
|
}
|
|
|
|
|
|
def get_servicer_dashboard_metrics(filter_type):
|
|
"""Get metrics for Servicer - Machine maintenance focused"""
|
|
# All machines they can service
|
|
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'
|
|
|
|
# Machines needing maintenance
|
|
maintenance_count = Machine.query.filter_by(operation_status='maintenance').count()
|
|
|
|
# Transaction count (all)
|
|
transactions_count = Transaction.query.count()
|
|
|
|
# Total sales
|
|
from sqlalchemy import func
|
|
total_sales = db.session.query(func.sum(Transaction.amount))\
|
|
.filter(Transaction.status == 'Success')\
|
|
.scalar() or 0.0
|
|
|
|
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}',
|
|
'maintenance_count': maintenance_count,
|
|
'user_role': 'servicer',
|
|
'user_type': 'Machine Maintenance'
|
|
}
|
|
|
|
# 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']:
|
|
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']:
|
|
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']:
|
|
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']:
|
|
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']:
|
|
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 |