2188 lines
79 KiB
Python
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 |