Replace submodules with full folder contents
20
Machine-Backend/.env
Normal file
@ -0,0 +1,20 @@
|
||||
SECRET_KEY=9d80e4410a5572c153bc8059e23b7fb6e2eeeb0bf1c4aff851cd60a3220efc6a
|
||||
JWT_SECRET_KEY=e57e8ed9d7af16276c128027d3c381380f39446fbe16bbd6fa8c73b0f64ac023
|
||||
|
||||
PAYU_MERCHANT_KEY=VqvG3m
|
||||
PAYU_MERCHANT_SALT=DusXSSjqqSMTPSpw32hlFXF6LKY2Zm3y
|
||||
PAYU_SUCCESS_URL=http://localhost:4200/payment-success
|
||||
PAYU_FAILURE_URL=http://localhost:4200/payment-failure
|
||||
|
||||
FLASK_ENV=production
|
||||
|
||||
MYSQL_HOST=db
|
||||
MYSQL_USER=vendinguser
|
||||
MYSQL_PASSWORD=vendingpass
|
||||
MYSQL_DATABASE=vending
|
||||
|
||||
SQLITE_DB_PATH=machines
|
||||
|
||||
BREVO_SMTP_EMAIL=smukeshsn2000@gmail.com
|
||||
BREVO_SMTP_KEY=your-brevo-smtp-key
|
||||
BREVO_SENDER_EMAIL=smukeshsn2000@gmail.com
|
||||
23
Machine-Backend/Dockerfile
Normal file
@ -0,0 +1,23 @@
|
||||
# Backend Dockerfile
|
||||
FROM python:3.10-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies (for mysqlclient or pymysql)
|
||||
RUN apt-get update && apt-get install -y gcc python3-dev default-libmysqlclient-dev && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy dependencies
|
||||
COPY requirements.txt ./
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy project files
|
||||
COPY . .
|
||||
|
||||
# Expose Flask port
|
||||
EXPOSE 5000
|
||||
|
||||
# Environment (set via docker-compose)
|
||||
ENV FLASK_ENV=production
|
||||
|
||||
# Start the Flask app
|
||||
CMD ["python", "run.py"]
|
||||
86
Machine-Backend/app/__init__.py
Normal file
@ -0,0 +1,86 @@
|
||||
import os
|
||||
from flask import Flask, send_from_directory
|
||||
from flask_cors import CORS
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
db = SQLAlchemy()
|
||||
|
||||
def create_app():
|
||||
app = Flask(__name__, instance_relative_config=True)
|
||||
|
||||
# CORS Configuration
|
||||
CORS(app, resources={
|
||||
r"/*": {
|
||||
"origins": ["http://localhost:4200", "http://localhost:4300"],
|
||||
"methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||
"allow_headers": ["Content-Type", "Authorization"],
|
||||
"supports_credentials": True
|
||||
}
|
||||
})
|
||||
|
||||
# Ensure directories exist
|
||||
instance_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'instance'))
|
||||
uploads_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'Uploads'))
|
||||
|
||||
for path in [instance_path, uploads_path]:
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path)
|
||||
|
||||
app.config['UPLOAD_FOLDER'] = uploads_path
|
||||
|
||||
# Database configuration
|
||||
flask_env = os.getenv('FLASK_ENV', 'development').lower()
|
||||
|
||||
if flask_env == 'production':
|
||||
mysql_host = os.getenv('MYSQL_HOST')
|
||||
mysql_user = os.getenv('MYSQL_USER')
|
||||
mysql_password = os.getenv('MYSQL_PASSWORD')
|
||||
mysql_db = os.getenv('MYSQL_DATABASE')
|
||||
|
||||
if not all([mysql_host, mysql_user, mysql_password, mysql_db]):
|
||||
raise ValueError("Missing required MySQL environment variables")
|
||||
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = (
|
||||
f'mysql+pymysql://{mysql_user}:{mysql_password}@{mysql_host}:3306/{mysql_db}'
|
||||
)
|
||||
else:
|
||||
sqlite_db_path = os.getenv('SQLITE_DB_PATH', 'machines.db')
|
||||
db_path = os.path.join(instance_path, sqlite_db_path)
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}'
|
||||
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
app.config['SQLALCHEMY_ECHO'] = os.getenv('SQLALCHEMY_ECHO', 'False').lower() == 'true'
|
||||
|
||||
# Initialize extensions
|
||||
db.init_app(app)
|
||||
|
||||
# Import models
|
||||
from app.models.models import Machine, User, Product, VendingSlot, Transaction
|
||||
|
||||
# Create tables
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
print(f"✓ Database initialized ({flask_env})")
|
||||
|
||||
# FIXED: Serial service connection BEFORE return
|
||||
from app.services.services import serial_service
|
||||
try:
|
||||
connected = serial_service.connect_to_machine('/dev/ttyUSB0', 9600)
|
||||
if connected:
|
||||
print("✓ Vending machine connected successfully")
|
||||
else:
|
||||
print("✗ Failed to connect to vending machine")
|
||||
except Exception as e:
|
||||
print(f"⚠ Serial connection error: {e}")
|
||||
|
||||
# Register blueprints
|
||||
from app.routes.routes import bp
|
||||
app.register_blueprint(bp, url_prefix='/')
|
||||
|
||||
@app.route('/uploads/<filename>')
|
||||
def uploaded_file(filename):
|
||||
return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
|
||||
|
||||
return app
|
||||
BIN
Machine-Backend/app/__pycache__/__init__.cpython-313.pyc
Normal file
12
Machine-Backend/app/instance/client_secrets.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"web": {
|
||||
"issuer": "http://localhost:8080/realms/myrealm",
|
||||
"auth_uri": "http://localhost:8080/realms/myrealm/protocol/openid-connect/auth",
|
||||
"client_id": "my-flask-app",
|
||||
"client_secret": "7kAD4BvMWbE7TP5f79sVX4NzzvsGyrFd",
|
||||
"redirect_uris": ["http://localhost:5000/oidc/callback"],
|
||||
"userinfo_uri": "http://localhost:8080/realms/myrealm/protocol/openid-connect/userinfo",
|
||||
"token_uri": "http://localhost:8080/realms/myrealm/protocol/openid-connect/token",
|
||||
"token_introspection_uri": "http://localhost:8080/realms/myrealm/protocol/openid-connect/token/introspect"
|
||||
}
|
||||
}
|
||||
BIN
Machine-Backend/app/instance/machines.db
Normal file
1
Machine-Backend/app/models/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .models import Machine, User, Product, VendingSlot
|
||||
BIN
Machine-Backend/app/models/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
Machine-Backend/app/models/__pycache__/machine.cpython-313.pyc
Normal file
BIN
Machine-Backend/app/models/__pycache__/models.cpython-313.pyc
Normal file
213
Machine-Backend/app/models/models.py
Normal file
@ -0,0 +1,213 @@
|
||||
from app import db
|
||||
import datetime
|
||||
import time
|
||||
import json
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
|
||||
# Machine Model
|
||||
class Machine(db.Model):
|
||||
__tablename__ = 'machines'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
machine_id = db.Column(db.String(10), unique=True, nullable=False)
|
||||
machine_model = db.Column(db.String(100), nullable=False)
|
||||
machine_type = db.Column(db.String(100), nullable=False)
|
||||
client_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
client_name = db.Column(db.String(100), nullable=True)
|
||||
branch_id = db.Column(db.String(10), nullable=False)
|
||||
branch_name = db.Column(db.String(100), nullable=False)
|
||||
operation_status = db.Column(db.String(50), nullable=False)
|
||||
connection_status = db.Column(db.String(50), nullable=False)
|
||||
created_on = db.Column(db.String(20), nullable=False)
|
||||
password = db.Column(db.String(128), nullable=False)
|
||||
slots = db.relationship('VendingSlot', backref='machine', lazy=True)
|
||||
client = db.relationship('User', backref='machines')
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'machine_id': self.machine_id,
|
||||
'machine_model': self.machine_model,
|
||||
'machine_type': self.machine_type,
|
||||
'client_id': self.client_id,
|
||||
'client_name': self.client.username if self.client else self.client_name,
|
||||
'branch_id': self.branch_id,
|
||||
'branch_name': self.branch_name,
|
||||
'operation_status': self.operation_status,
|
||||
'connection_status': self.connection_status,
|
||||
'created_on': self.created_on,
|
||||
'password': self.password
|
||||
}
|
||||
|
||||
def set_password(self, password):
|
||||
self.password = generate_password_hash(password)
|
||||
|
||||
def check_password(self, password):
|
||||
return check_password_hash(self.password, password)
|
||||
|
||||
|
||||
# User Model - UPDATED with proper password hashing
|
||||
class User(db.Model):
|
||||
__tablename__ = 'users'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.String(50), unique=True, nullable=False)
|
||||
username = db.Column(db.String(50), nullable=False)
|
||||
email = db.Column(db.String(100), nullable=False, unique=True)
|
||||
contact = db.Column(db.String(20), nullable=False)
|
||||
roles = db.Column(db.String(50), nullable=False)
|
||||
user_status = db.Column(db.String(50), nullable=False)
|
||||
password = db.Column(db.String(255), nullable=False) # Increased length for hash
|
||||
|
||||
# File storage fields
|
||||
photo = db.Column(db.String(255), nullable=True)
|
||||
company_logo = db.Column(db.String(255), nullable=True)
|
||||
documents = db.Column(db.Text, nullable=True)
|
||||
|
||||
# Timestamps
|
||||
created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert user object to dictionary"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'user_id': self.user_id,
|
||||
'username': self.username,
|
||||
'email': self.email,
|
||||
'contact': self.contact,
|
||||
'roles': self.roles,
|
||||
'user_status': self.user_status,
|
||||
'photo': self.photo,
|
||||
'company_logo': self.company_logo,
|
||||
'documents': json.loads(self.documents) if self.documents else [],
|
||||
'created_at': self.created_at.strftime("%Y-%m-%d %H:%M:%S") if self.created_at else None,
|
||||
'updated_at': self.updated_at.strftime("%Y-%m-%d %H:%M:%S") if self.updated_at else None,
|
||||
'machines': [m.to_dict() for m in self.machines] if hasattr(self, 'machines') else []
|
||||
}
|
||||
|
||||
def set_password(self, password):
|
||||
"""Hash and set the password"""
|
||||
self.password = generate_password_hash(password, method='pbkdf2:sha256')
|
||||
print(f"Password hashed for user {self.username}")
|
||||
|
||||
def check_password(self, password):
|
||||
"""Verify password against hash"""
|
||||
result = check_password_hash(self.password, password)
|
||||
print(f"Password check for {self.username}: {result}")
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def generate_user_id(username, email):
|
||||
"""Generate unique user ID from username and email"""
|
||||
username_part = ''.join(filter(str.isalnum, username[:4])).upper()
|
||||
email_part = ''.join(filter(str.isalnum, email.split('@')[0][:4])).upper()
|
||||
timestamp = str(int(time.time()))[-4:]
|
||||
user_id = f"USR-{username_part}{email_part}{timestamp}"
|
||||
|
||||
# Ensure uniqueness
|
||||
counter = 1
|
||||
original_id = user_id
|
||||
while User.query.filter_by(user_id=user_id).first():
|
||||
user_id = f"{original_id}{counter}"
|
||||
counter += 1
|
||||
|
||||
return user_id
|
||||
|
||||
|
||||
# Product Model
|
||||
class Product(db.Model):
|
||||
__tablename__ = 'products'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
product_id = db.Column(db.String(10), unique=True, nullable=False)
|
||||
product_name = db.Column(db.String(100), nullable=False)
|
||||
price = db.Column(db.Float, nullable=False)
|
||||
product_image = db.Column(db.String(255), nullable=False)
|
||||
created_date = db.Column(db.String(20), nullable=False)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'product_id': self.product_id,
|
||||
'product_name': self.product_name,
|
||||
'price': str(self.price),
|
||||
'product_image': self.product_image,
|
||||
'created_date': self.created_date
|
||||
}
|
||||
|
||||
|
||||
# VendingSlot Model
|
||||
class VendingSlot(db.Model):
|
||||
__tablename__ = 'vending_slots'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
machine_id = db.Column(db.String(10), db.ForeignKey('machines.machine_id'), nullable=False)
|
||||
row_id = db.Column(db.String(1), nullable=False)
|
||||
slot_name = db.Column(db.String(3), nullable=False)
|
||||
enabled = db.Column(db.Boolean, default=True)
|
||||
product_id = db.Column(db.String(10), db.ForeignKey('products.product_id'), nullable=True)
|
||||
units = db.Column(db.Integer, default=0)
|
||||
price = db.Column(db.String(10), default="N/A")
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'name': self.slot_name,
|
||||
'enabled': self.enabled,
|
||||
'productId': self.product_id,
|
||||
'units': self.units,
|
||||
'price': self.price
|
||||
}
|
||||
|
||||
# Transaction Model
|
||||
class Transaction(db.Model):
|
||||
__tablename__ = 'transactions'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
transaction_id = db.Column(db.String(50), unique=True, nullable=False)
|
||||
machine_id = db.Column(db.String(10), db.ForeignKey('machines.machine_id'), nullable=False)
|
||||
product_name = db.Column(db.String(100), nullable=False)
|
||||
quantity = db.Column(db.Integer, nullable=False)
|
||||
amount = db.Column(db.Float, nullable=False)
|
||||
payment_type = db.Column(db.String(50), nullable=False) # wallet/qr/card
|
||||
status = db.Column(db.String(50), nullable=False) # Success/Dispense failed/Unprocessed
|
||||
amount_receiving_status = db.Column(db.String(50), nullable=True) # Dispense failed refund
|
||||
return_amount = db.Column(db.Float, nullable=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
machine = db.relationship('Machine', backref='transactions')
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'transaction_id': self.transaction_id,
|
||||
'machine_id': self.machine_id,
|
||||
'product_name': self.product_name,
|
||||
'quantity': self.quantity,
|
||||
'amount': self.amount,
|
||||
'payment_type': self.payment_type,
|
||||
'status': self.status,
|
||||
'amount_receiving_status': self.amount_receiving_status,
|
||||
'return_amount': self.return_amount,
|
||||
'created_at': self.created_at.strftime("%Y-%m-%d %H:%M:%S") if self.created_at else None
|
||||
}
|
||||
# Add to your models.py
|
||||
class Role(db.Model):
|
||||
__tablename__ = 'roles'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(50), unique=True, nullable=False)
|
||||
description = db.Column(db.String(255))
|
||||
permissions = db.Column(db.Text) # JSON string of permission IDs
|
||||
created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'description': self.description,
|
||||
'permissions': json.loads(self.permissions) if self.permissions else [],
|
||||
'created_at': self.created_at.strftime("%Y-%m-%d %H:%M:%S") if self.created_at else None,
|
||||
'updated_at': self.updated_at.strftime("%Y-%m-%d %H:%M:%S") if self.updated_at else None
|
||||
}
|
||||
1
Machine-Backend/app/routes/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .routes import bp
|
||||
BIN
Machine-Backend/app/routes/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
Machine-Backend/app/routes/__pycache__/routes.cpython-313.pyc
Normal file
1724
Machine-Backend/app/routes/routes.py
Normal file
1
Machine-Backend/app/services/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .services import MachineService, UserService, ProductService
|
||||
967
Machine-Backend/app/services/services.py
Normal file
@ -0,0 +1,967 @@
|
||||
import uuid
|
||||
import datetime
|
||||
import datetime
|
||||
import re
|
||||
import os
|
||||
import json
|
||||
import serial
|
||||
import time
|
||||
import threading
|
||||
from typing import Optional, Dict, Any
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from app import db
|
||||
from app.models.models import Machine, User, Product, VendingSlot, Transaction, Role
|
||||
from flask import current_app
|
||||
import secrets
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
class SerialCommunicationService:
|
||||
def __init__(self):
|
||||
self.serial_port: Optional[serial.Serial] = None
|
||||
self.is_connected = False
|
||||
self.response_buffer = ""
|
||||
self.last_response = None
|
||||
self.response_lock = threading.Lock()
|
||||
|
||||
def connect_to_machine(self, port: str = '/dev/ttyUSB0', baudrate: int = 9600) -> bool:
|
||||
"""
|
||||
Connect to vending machine via serial port
|
||||
Common ports:
|
||||
- Linux: /dev/ttyUSB0, /dev/ttyACM0
|
||||
- Windows: COM3, COM4, etc.
|
||||
- macOS: /dev/cu.usbserial-*
|
||||
"""
|
||||
try:
|
||||
self.serial_port = serial.Serial(
|
||||
port=port,
|
||||
baudrate=baudrate,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
parity=serial.PARITY_NONE,
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
timeout=1 # 1 second timeout for reads
|
||||
)
|
||||
|
||||
# Test connection with a status command
|
||||
time.sleep(2) # Allow time for Arduino to reset
|
||||
self.serial_port.write(b'STATUS\n')
|
||||
time.sleep(1)
|
||||
|
||||
if self.serial_port.in_waiting > 0:
|
||||
response = self.serial_port.readline().decode('utf-8').strip()
|
||||
print(f"Machine connected successfully. Status: {response}")
|
||||
self.is_connected = True
|
||||
return True
|
||||
else:
|
||||
print("No response from machine")
|
||||
self.serial_port.close()
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to connect to machine: {e}")
|
||||
self.is_connected = False
|
||||
return False
|
||||
|
||||
def send_dispense_command(self, slot_id: str, timeout: int = 10) -> Dict[str, Any]:
|
||||
"""
|
||||
Send dispense command and wait for drop sensor response
|
||||
"""
|
||||
if not self.is_connected or not self.serial_port:
|
||||
return {
|
||||
'success': False,
|
||||
'error': 'Not connected to machine',
|
||||
'response': 'NO_CONNECTION'
|
||||
}
|
||||
|
||||
try:
|
||||
# Clear any pending data
|
||||
self.serial_port.reset_input_buffer()
|
||||
|
||||
# Send dispense command
|
||||
command = f"DISPENSE:{slot_id}\n"
|
||||
self.serial_port.write(command.encode('utf-8'))
|
||||
print(f"Sent command: {command.strip()}")
|
||||
|
||||
# Wait for response with timeout
|
||||
start_time = time.time()
|
||||
response = ""
|
||||
|
||||
while time.time() - start_time < timeout:
|
||||
if self.serial_port.in_waiting > 0:
|
||||
line = self.serial_port.readline().decode('utf-8').strip()
|
||||
print(f"Received: {line}")
|
||||
|
||||
if line in ['PRODUCT_DISPENSED', 'DISPENSE_FAILED', 'SLOT_EMPTY', 'MOTOR_ERROR']:
|
||||
response = line
|
||||
break
|
||||
|
||||
time.sleep(0.1) # Small delay to prevent busy waiting
|
||||
|
||||
if not response:
|
||||
return {
|
||||
'success': False,
|
||||
'error': 'Timeout waiting for drop sensor',
|
||||
'response': 'TIMEOUT'
|
||||
}
|
||||
|
||||
success = response == 'PRODUCT_DISPENSED'
|
||||
return {
|
||||
'success': success,
|
||||
'response': response,
|
||||
'slot_id': slot_id,
|
||||
'message': self._get_response_message(response, slot_id)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error during dispensing: {e}")
|
||||
return {
|
||||
'success': False,
|
||||
'error': str(e),
|
||||
'response': 'COMMUNICATION_ERROR'
|
||||
}
|
||||
|
||||
def _get_response_message(self, response: str, slot_id: str) -> str:
|
||||
"""Get user-friendly message for response codes"""
|
||||
messages = {
|
||||
'PRODUCT_DISPENSED': f'Product successfully dispensed from slot {slot_id}',
|
||||
'DISPENSE_FAILED': f'Dispensing failed - no product detected from slot {slot_id}',
|
||||
'SLOT_EMPTY': f'Slot {slot_id} is empty',
|
||||
'MOTOR_ERROR': f'Motor malfunction in slot {slot_id}',
|
||||
'TIMEOUT': f'Timeout - no response from slot {slot_id}',
|
||||
'COMMUNICATION_ERROR': 'Communication error with machine'
|
||||
}
|
||||
return messages.get(response, f'Unknown response: {response}')
|
||||
|
||||
def check_machine_status(self) -> Dict[str, Any]:
|
||||
"""Check overall machine status"""
|
||||
if not self.is_connected or not self.serial_port:
|
||||
return {'status': 'disconnected', 'message': 'Not connected to machine'}
|
||||
|
||||
try:
|
||||
self.serial_port.reset_input_buffer()
|
||||
self.serial_port.write(b'STATUS\n')
|
||||
time.sleep(1)
|
||||
|
||||
if self.serial_port.in_waiting > 0:
|
||||
response = self.serial_port.readline().decode('utf-8').strip()
|
||||
return {
|
||||
'status': 'connected',
|
||||
'response': response,
|
||||
'message': 'Machine is responding'
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'status': 'no_response',
|
||||
'message': 'Machine not responding'
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
'status': 'error',
|
||||
'error': str(e),
|
||||
'message': 'Communication error'
|
||||
}
|
||||
|
||||
def reset_machine(self) -> Dict[str, Any]:
|
||||
"""Send reset command to clear any error states"""
|
||||
if not self.is_connected or not self.serial_port:
|
||||
return {'success': False, 'error': 'Not connected to machine'}
|
||||
|
||||
try:
|
||||
self.serial_port.write(b'RESET\n')
|
||||
time.sleep(2)
|
||||
|
||||
response = self.serial_port.readline().decode('utf-8').strip()
|
||||
return {
|
||||
'success': True,
|
||||
'response': response,
|
||||
'message': 'Reset command sent'
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {'success': False, 'error': str(e)}
|
||||
|
||||
def disconnect(self):
|
||||
"""Disconnect from serial port"""
|
||||
if self.serial_port and self.serial_port.is_open:
|
||||
self.serial_port.close()
|
||||
self.is_connected = False
|
||||
print("Disconnected from vending machine")
|
||||
|
||||
# Global instance
|
||||
serial_service = SerialCommunicationService()
|
||||
|
||||
# Machine Service (UNCHANGED)
|
||||
class MachineService:
|
||||
@staticmethod
|
||||
def generate_id(prefix):
|
||||
return f"{prefix}{uuid.uuid4().hex[:8].upper()}"
|
||||
|
||||
@staticmethod
|
||||
def generate_unique_password():
|
||||
while True:
|
||||
password = secrets.token_hex(4).upper() # e.g., "A1B2C3D4"
|
||||
if not Machine.query.filter_by(password=password).first():
|
||||
return password
|
||||
|
||||
@staticmethod
|
||||
def validate_machine_data(data):
|
||||
required_fields = ['machine_model', 'machine_type', 'client_name', 'branch_name', 'operation_status', 'connection_status']
|
||||
for field in required_fields:
|
||||
if field not in data:
|
||||
raise ValueError(f"Missing required field: {field}")
|
||||
|
||||
status_mapping = {
|
||||
'connected': 'connected',
|
||||
'disconnected': 'disconnected',
|
||||
'maintenance':'maintenance',
|
||||
'active': 'active',
|
||||
'inactive': 'inactive'
|
||||
}
|
||||
|
||||
if data['operation_status'] not in status_mapping:
|
||||
raise ValueError(f"Invalid operation status. Allowed statuses: {list(status_mapping.keys())}")
|
||||
data['operation_status'] = status_mapping[data['operation_status']]
|
||||
|
||||
if data['connection_status'] not in status_mapping:
|
||||
raise ValueError(f"Invalid connection status. Allowed statuses: {list(status_mapping.keys())}")
|
||||
data['connection_status'] = status_mapping[data['connection_status']]
|
||||
|
||||
@staticmethod
|
||||
def get_all_machines():
|
||||
return Machine.query.all()
|
||||
|
||||
@staticmethod
|
||||
def get_machine_slots(machine_id: str):
|
||||
machine = Machine.query.filter_by(machine_id=machine_id).first()
|
||||
if not machine:
|
||||
return None
|
||||
|
||||
slots = VendingSlot.query.filter_by(machine_id=machine_id).all()
|
||||
|
||||
product_ids = [slot.product_id for slot in slots if slot.product_id]
|
||||
products = {p.product_id: p for p in Product.query.filter(Product.product_id.in_(product_ids)).all()} if product_ids else {}
|
||||
|
||||
return [{
|
||||
'slot_name': slot.row_id,
|
||||
'column': int(slot.slot_name[1:]) if slot.slot_name[1:].isdigit() else 1,
|
||||
'enabled': slot.enabled,
|
||||
'product_id': slot.product_id,
|
||||
'units': slot.units or 0,
|
||||
'price': slot.price or '0.0',
|
||||
'product_name': products.get(slot.product_id, {'product_name': 'N/A'}).product_name if slot.product_id else 'N/A',
|
||||
'product_image': products.get(slot.product_id, {'product_image': None}).product_image if slot.product_id else None
|
||||
} for slot in slots]
|
||||
|
||||
@staticmethod
|
||||
def create_machine(data):
|
||||
# Modified validation to accept client_id instead of client_name
|
||||
required_fields = ['machine_model', 'machine_type', 'client_id', 'branch_name', 'operation_status', 'connection_status']
|
||||
for field in required_fields:
|
||||
if field not in data:
|
||||
raise ValueError(f"Missing required field: {field}")
|
||||
|
||||
status_mapping = {
|
||||
'connected': 'connected',
|
||||
'disconnected': 'disconnected',
|
||||
'maintenance':'maintenance',
|
||||
'active': 'active',
|
||||
'inactive': 'inactive'
|
||||
}
|
||||
|
||||
if data['operation_status'] not in status_mapping:
|
||||
raise ValueError(f"Invalid operation status. Allowed statuses: {list(status_mapping.keys())}")
|
||||
data['operation_status'] = status_mapping[data['operation_status']]
|
||||
|
||||
if data['connection_status'] not in status_mapping:
|
||||
raise ValueError(f"Invalid connection status. Allowed statuses: {list(status_mapping.keys())}")
|
||||
data['connection_status'] = status_mapping[data['connection_status']]
|
||||
|
||||
machine_id = MachineService.generate_id("M")
|
||||
branch_id = MachineService.generate_id("B")
|
||||
created_on = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
password = MachineService.generate_unique_password()
|
||||
|
||||
# Validate client_id exists in users
|
||||
client_id = data.get('client_id')
|
||||
if not client_id or not User.query.get(client_id):
|
||||
raise ValueError("Invalid or missing client_id")
|
||||
|
||||
# Get client_name from user
|
||||
user = User.query.get(client_id)
|
||||
client_name = user.username
|
||||
|
||||
new_machine = Machine(
|
||||
machine_id=machine_id,
|
||||
machine_model=data['machine_model'],
|
||||
machine_type=data['machine_type'],
|
||||
client_id=client_id,
|
||||
client_name=client_name, # Set client_name from user record
|
||||
branch_id=branch_id,
|
||||
branch_name=data['branch_name'],
|
||||
operation_status=data['operation_status'],
|
||||
connection_status=data['connection_status'],
|
||||
created_on=created_on,
|
||||
password=password
|
||||
)
|
||||
|
||||
db.session.add(new_machine)
|
||||
db.session.commit()
|
||||
return new_machine
|
||||
|
||||
@staticmethod
|
||||
def update_machine(id, data):
|
||||
machine = Machine.query.get_or_404(id)
|
||||
|
||||
if any(field in data for field in ['machine_model', 'machine_type', 'branch_name', 'operation_status', 'connection_status']):
|
||||
MachineService.validate_machine_data({k: data.get(k, getattr(machine, k)) for k in ['machine_model', 'machine_type', 'branch_name', 'operation_status', 'connection_status']})
|
||||
|
||||
machine.machine_model = data.get('machine_model', machine.machine_model)
|
||||
machine.machine_type = data.get('machine_type', machine.machine_type)
|
||||
if 'client_id' in data:
|
||||
client_id = data['client_id']
|
||||
if not User.query.get(client_id):
|
||||
raise ValueError("Invalid client_id")
|
||||
machine.client_id = client_id
|
||||
machine.branch_name = data.get('branch_name', machine.branch_name)
|
||||
machine.operation_status = data.get('operation_status', machine.operation_status)
|
||||
machine.connection_status = data.get('connection_status', machine.connection_status)
|
||||
|
||||
db.session.commit()
|
||||
return machine
|
||||
|
||||
@staticmethod
|
||||
def delete_machine(id):
|
||||
machine = Machine.query.get_or_404(id)
|
||||
db.session.delete(machine)
|
||||
db.session.commit()
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def get_machine_by_id(id):
|
||||
return Machine.query.get_or_404(id)
|
||||
|
||||
@staticmethod
|
||||
def get_vending_state(machine_id):
|
||||
machine = Machine.query.filter_by(machine_id=machine_id).first_or_404()
|
||||
slots = VendingSlot.query.filter_by(machine_id=machine_id).all()
|
||||
|
||||
vending_rows = {}
|
||||
for slot in slots:
|
||||
if slot.row_id not in vending_rows:
|
||||
vending_rows[slot.row_id] = []
|
||||
vending_rows[slot.row_id].append(slot.to_dict())
|
||||
|
||||
return {
|
||||
'vendingRows': [{'rowId': row_id, 'slots': slots} for row_id, slots in vending_rows.items()],
|
||||
'lastUpdated': datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def update_vending_state(machine_id, vending_rows):
|
||||
machine = Machine.query.filter_by(machine_id=machine_id).first_or_404()
|
||||
VendingSlot.query.filter_by(machine_id=machine_id).delete()
|
||||
|
||||
for row in vending_rows:
|
||||
row_id = row['rowId']
|
||||
for slot_data in row['slots']:
|
||||
new_slot = VendingSlot(
|
||||
machine_id=machine_id,
|
||||
row_id=row_id,
|
||||
slot_name=slot_data['name'],
|
||||
enabled=slot_data['enabled'],
|
||||
product_id=slot_data['productId'],
|
||||
units=slot_data['units'],
|
||||
price=slot_data['price']
|
||||
)
|
||||
db.session.add(new_slot)
|
||||
|
||||
db.session.commit()
|
||||
return {'success': True, 'message': 'Vending state updated', 'lastUpdated': datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
||||
|
||||
# User Service (MODIFIED FOR BREVO SMTP WITH ENV VARIABLES)
|
||||
# Updated UserService in services.py
|
||||
class UserService:
|
||||
@staticmethod
|
||||
def generate_unique_password():
|
||||
"""Generate a secure random password"""
|
||||
return secrets.token_hex(4).upper() # 8 character hex password
|
||||
|
||||
@staticmethod
|
||||
def save_file(file, folder='user_uploads'):
|
||||
"""Save uploaded file and return path"""
|
||||
if not file:
|
||||
return None
|
||||
|
||||
filename = f"{uuid.uuid4().hex}_{file.filename}"
|
||||
upload_folder = os.path.join(current_app.config['UPLOAD_FOLDER'], folder)
|
||||
|
||||
os.makedirs(upload_folder, exist_ok=True)
|
||||
|
||||
file_path = os.path.join(upload_folder, filename)
|
||||
file.save(file_path)
|
||||
|
||||
return f"/uploads/{folder}/{filename}"
|
||||
|
||||
@staticmethod
|
||||
def save_multiple_documents(files, metadata_json=None):
|
||||
"""Save multiple document files with metadata"""
|
||||
document_paths = []
|
||||
|
||||
metadata_list = []
|
||||
if metadata_json:
|
||||
try:
|
||||
metadata_list = json.loads(metadata_json)
|
||||
print(f"Parsed metadata: {metadata_list}")
|
||||
except Exception as e:
|
||||
print(f"Error parsing metadata: {e}")
|
||||
metadata_list = []
|
||||
|
||||
if not isinstance(files, list):
|
||||
files = [files]
|
||||
|
||||
for idx, file in enumerate(files):
|
||||
if file and hasattr(file, 'filename'):
|
||||
try:
|
||||
path = UserService.save_file(file, 'user_documents')
|
||||
if path:
|
||||
metadata = metadata_list[idx] if idx < len(metadata_list) else {}
|
||||
|
||||
document_paths.append({
|
||||
'filename': file.filename,
|
||||
'path': path,
|
||||
'uploaded_at': datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
'document_type': metadata.get('document_type', 'other'),
|
||||
'document_type_other': metadata.get('document_type_other', '')
|
||||
})
|
||||
print(f"Saved document: {file.filename}")
|
||||
except Exception as e:
|
||||
print(f"Error saving file {file.filename}: {e}")
|
||||
continue
|
||||
|
||||
return document_paths
|
||||
|
||||
@staticmethod
|
||||
def send_email_notification(username, email, contact, password, user_id):
|
||||
"""Send email notification with user credentials"""
|
||||
subject = "Your Account Credentials"
|
||||
body = f"""
|
||||
Dear {username},
|
||||
|
||||
Your account has been created successfully!
|
||||
|
||||
Login Credentials:
|
||||
==================
|
||||
User ID: {user_id}
|
||||
Username: {username}
|
||||
Email: {email}
|
||||
Contact: {contact}
|
||||
Password: {password}
|
||||
|
||||
Important Security Notice:
|
||||
- Please change your password after first login
|
||||
- Keep this information secure and confidential
|
||||
- Do not share your credentials with anyone
|
||||
|
||||
You can login using either your email or username along with your password.
|
||||
|
||||
Best regards,
|
||||
System Administration Team
|
||||
"""
|
||||
|
||||
msg = MIMEText(body)
|
||||
msg['Subject'] = subject
|
||||
msg['From'] = os.getenv('BREVO_SENDER_EMAIL')
|
||||
msg['To'] = email
|
||||
|
||||
smtp_email = os.getenv('BREVO_SMTP_EMAIL')
|
||||
smtp_key = os.getenv('BREVO_SMTP_KEY')
|
||||
|
||||
if not all([smtp_email, smtp_key, msg['From']]):
|
||||
print("Error: Brevo SMTP configuration missing in .env")
|
||||
return False
|
||||
|
||||
try:
|
||||
with smtplib.SMTP('smtp-relay.brevo.com', 587) as server:
|
||||
server.starttls()
|
||||
server.login(smtp_email, smtp_key)
|
||||
server.send_message(msg)
|
||||
print(f"✓ Email sent successfully to {email}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"✗ Failed to send email to {email}: {str(e)}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def log_sms_notification(contact, username, email, password, user_id):
|
||||
"""Log SMS notification (for future SMS integration)"""
|
||||
notification = {
|
||||
"timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"user_id": user_id,
|
||||
"username": username,
|
||||
"email": email,
|
||||
"contact": contact,
|
||||
"password": password,
|
||||
"message": f"Your account credentials: User ID: {user_id}, Username: {username}, Email: {email}, Password: {password}"
|
||||
}
|
||||
|
||||
notifications_file = os.path.join(current_app.root_path, "sms_notifications.json")
|
||||
|
||||
try:
|
||||
notifications = []
|
||||
if os.path.exists(notifications_file):
|
||||
with open(notifications_file, "r") as f:
|
||||
notifications = json.load(f)
|
||||
|
||||
notifications.append(notification)
|
||||
|
||||
with open(notifications_file, "w") as f:
|
||||
json.dump(notifications, f, indent=4)
|
||||
|
||||
print(f"✓ SMS notification logged for {contact}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"✗ Failed to log SMS notification: {str(e)}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def validate_user_data(data):
|
||||
"""Validate user data before creating/updating"""
|
||||
import re
|
||||
|
||||
required_fields = ['username', 'email', 'contact', 'roles', 'user_status']
|
||||
for field in required_fields:
|
||||
if field not in data:
|
||||
raise ValueError(f"Missing required field: {field}")
|
||||
|
||||
# Email validation - FIXED: Added closing quote
|
||||
email_regex = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||||
if not re.match(email_regex, data['email']):
|
||||
raise ValueError("Invalid email format")
|
||||
|
||||
@staticmethod
|
||||
def get_all_users():
|
||||
"""Get all users from database"""
|
||||
return User.query.all()
|
||||
|
||||
@staticmethod
|
||||
def create_user(data, photo_file=None, logo_file=None, document_files=None, documents_metadata=None):
|
||||
"""
|
||||
Create new user with proper password hashing
|
||||
Password must be provided in data
|
||||
"""
|
||||
print(f"\n{'='*60}")
|
||||
print(f"CREATING NEW USER")
|
||||
print(f"{'='*60}")
|
||||
|
||||
# Validate user data
|
||||
UserService.validate_user_data(data)
|
||||
|
||||
# CHECK: Password must be provided
|
||||
if 'password' not in data or not data['password']:
|
||||
raise ValueError("Password is required")
|
||||
|
||||
# Generate user ID only
|
||||
user_id = User.generate_user_id(data['username'], data['email'])
|
||||
password = data['password'] # Use provided password, not auto-generated
|
||||
|
||||
print(f"Generated User ID: {user_id}")
|
||||
print(f"Password provided by admin")
|
||||
|
||||
# Handle file uploads
|
||||
photo_path = UserService.save_file(photo_file, 'user_photos') if photo_file else None
|
||||
logo_path = UserService.save_file(logo_file, 'company_logos') if logo_file else None
|
||||
documents = UserService.save_multiple_documents(document_files, documents_metadata) if document_files else []
|
||||
|
||||
# Create new user
|
||||
new_user = User(
|
||||
user_id=user_id,
|
||||
username=data['username'],
|
||||
email=data['email'],
|
||||
contact=data['contact'],
|
||||
roles=data['roles'],
|
||||
user_status=data['user_status'],
|
||||
photo=photo_path,
|
||||
company_logo=logo_path,
|
||||
documents=json.dumps(documents)
|
||||
)
|
||||
|
||||
# Set password (this will hash it automatically)
|
||||
new_user.set_password(password)
|
||||
print(f"Password hashed and set for user")
|
||||
|
||||
# Save to database
|
||||
db.session.add(new_user)
|
||||
db.session.commit()
|
||||
|
||||
print(f"✓ User created successfully with ID: {new_user.id}")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
# Optional: Send email notification with the password they set
|
||||
# UserService.send_email_notification(...)
|
||||
|
||||
return new_user
|
||||
|
||||
@staticmethod
|
||||
def update_user(id, data, photo_file=None, logo_file=None, document_files=None, documents_metadata=None):
|
||||
"""
|
||||
Update existing user
|
||||
Password is only updated if explicitly provided in data
|
||||
"""
|
||||
user = User.query.get_or_404(id)
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"UPDATING USER: {user.username}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
# Validate only if fields are being updated
|
||||
if any(field in data for field in ['username', 'email', 'contact', 'roles', 'user_status']):
|
||||
UserService.validate_user_data({
|
||||
'username': data.get('username', user.username),
|
||||
'email': data.get('email', user.email),
|
||||
'contact': data.get('contact', user.contact),
|
||||
'roles': data.get('roles', user.roles),
|
||||
'user_status': data.get('user_status', user.user_status)
|
||||
})
|
||||
|
||||
# Update basic fields
|
||||
user.username = data.get('username', user.username)
|
||||
user.email = data.get('email', user.email)
|
||||
user.contact = data.get('contact', user.contact)
|
||||
user.roles = data.get('roles', user.roles)
|
||||
user.user_status = data.get('user_status', user.user_status)
|
||||
|
||||
# Update password if provided (will be hashed automatically)
|
||||
if 'password' in data and data['password']:
|
||||
print(f"Updating password for user")
|
||||
user.set_password(data['password'])
|
||||
|
||||
# Handle file updates
|
||||
if photo_file:
|
||||
if user.photo:
|
||||
old_path = os.path.join(current_app.config['UPLOAD_FOLDER'],
|
||||
user.photo.replace('/uploads/', ''))
|
||||
if os.path.exists(old_path):
|
||||
os.remove(old_path)
|
||||
user.photo = UserService.save_file(photo_file, 'user_photos')
|
||||
|
||||
if logo_file:
|
||||
if user.company_logo:
|
||||
old_path = os.path.join(current_app.config['UPLOAD_FOLDER'],
|
||||
user.company_logo.replace('/uploads/', ''))
|
||||
if os.path.exists(old_path):
|
||||
os.remove(old_path)
|
||||
user.company_logo = UserService.save_file(logo_file, 'company_logos')
|
||||
|
||||
if document_files:
|
||||
existing_docs = json.loads(user.documents) if user.documents else []
|
||||
new_docs = UserService.save_multiple_documents(document_files, documents_metadata)
|
||||
existing_docs.extend(new_docs)
|
||||
user.documents = json.dumps(existing_docs)
|
||||
|
||||
user.updated_at = datetime.datetime.utcnow()
|
||||
|
||||
db.session.commit()
|
||||
|
||||
print(f"✓ User updated successfully")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
def delete_user_document(user_id, document_path):
|
||||
"""Delete a specific document from a user"""
|
||||
user = User.query.get_or_404(user_id)
|
||||
|
||||
if user.documents:
|
||||
documents = json.loads(user.documents)
|
||||
documents = [doc for doc in documents if doc['path'] != document_path]
|
||||
user.documents = json.dumps(documents)
|
||||
|
||||
# Delete physical file
|
||||
file_path = os.path.join(current_app.config['UPLOAD_FOLDER'],
|
||||
document_path.replace('/uploads/', ''))
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
|
||||
db.session.commit()
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def delete_user(id):
|
||||
"""Delete user and all associated files"""
|
||||
user = User.query.get_or_404(id)
|
||||
|
||||
# Delete all associated files
|
||||
if user.photo:
|
||||
photo_path = os.path.join(current_app.config['UPLOAD_FOLDER'],
|
||||
user.photo.replace('/uploads/', ''))
|
||||
if os.path.exists(photo_path):
|
||||
os.remove(photo_path)
|
||||
|
||||
if user.company_logo:
|
||||
logo_path = os.path.join(current_app.config['UPLOAD_FOLDER'],
|
||||
user.company_logo.replace('/uploads/', ''))
|
||||
if os.path.exists(logo_path):
|
||||
os.remove(logo_path)
|
||||
|
||||
if user.documents:
|
||||
documents = json.loads(user.documents)
|
||||
for doc in documents:
|
||||
doc_path = os.path.join(current_app.config['UPLOAD_FOLDER'],
|
||||
doc['path'].replace('/uploads/', ''))
|
||||
if os.path.exists(doc_path):
|
||||
os.remove(doc_path)
|
||||
|
||||
db.session.delete(user)
|
||||
db.session.commit()
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def validate_file(file, maxSizeMB=10):
|
||||
"""Validate file size"""
|
||||
max_size_bytes = maxSizeMB * 1024 * 1024
|
||||
if file.size > max_size_bytes:
|
||||
return f"File size exceeds {maxSizeMB}MB limit"
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def is_valid_image_type(file):
|
||||
"""Check if file is valid image type"""
|
||||
valid_types = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif']
|
||||
return valid_types.includes(file.type) if hasattr(file, 'type') else False
|
||||
|
||||
@staticmethod
|
||||
def is_valid_document_type(file):
|
||||
"""Check if file is valid document type"""
|
||||
valid_types = [
|
||||
'application/pdf',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'image/jpeg',
|
||||
'image/jpg',
|
||||
'image/png'
|
||||
]
|
||||
return valid_types.includes(file.type) if hasattr(file, 'type') else False
|
||||
|
||||
# Product Service (UNCHANGED)
|
||||
class ProductService:
|
||||
@staticmethod
|
||||
def generate_id(prefix):
|
||||
return f"{prefix}{uuid.uuid4().hex[:4].upper()}"
|
||||
|
||||
@staticmethod
|
||||
def validate_product_data(data):
|
||||
required_fields = ['product_name', 'price']
|
||||
for field in required_fields:
|
||||
if field not in data or not data[field]:
|
||||
raise ValueError(f"Missing or empty required field: {field}")
|
||||
|
||||
try:
|
||||
price = float(data['price'])
|
||||
if price < 0:
|
||||
raise ValueError("Price must be a positive number")
|
||||
except (ValueError, TypeError):
|
||||
raise ValueError("Price must be a valid number")
|
||||
|
||||
@staticmethod
|
||||
def save_image(file):
|
||||
if not file:
|
||||
raise ValueError("Product image is required")
|
||||
|
||||
filename = f"{uuid.uuid4().hex}_{file.filename}"
|
||||
file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], filename)
|
||||
|
||||
file.save(file_path)
|
||||
|
||||
return f"/uploads/{filename}"
|
||||
|
||||
@staticmethod
|
||||
def get_all_products():
|
||||
return Product.query.all()
|
||||
|
||||
@staticmethod
|
||||
def create_product(data, file):
|
||||
ProductService.validate_product_data(data)
|
||||
|
||||
image_path = ProductService.save_image(file)
|
||||
|
||||
product_id = ProductService.generate_id("P")
|
||||
created_date = data.get('created_date', datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
||||
|
||||
new_product = Product(
|
||||
product_id=product_id,
|
||||
product_name=data['product_name'],
|
||||
price=float(data['price']),
|
||||
product_image=image_path,
|
||||
created_date=created_date
|
||||
)
|
||||
|
||||
db.session.add(new_product)
|
||||
db.session.commit()
|
||||
return new_product
|
||||
|
||||
@staticmethod
|
||||
def update_product(id, data, file):
|
||||
product = Product.query.get_or_404(id)
|
||||
|
||||
ProductService.validate_product_data(data)
|
||||
|
||||
product.product_name = data.get('product_name', product.product_name)
|
||||
product.price = float(data.get('price', product.price))
|
||||
product.created_date = data.get('created_date', product.created_date)
|
||||
|
||||
if file:
|
||||
if product.product_image:
|
||||
old_image_path = os.path.join(current_app.config['UPLOAD_FOLDER'], product.product_image.split('/')[-1])
|
||||
if os.path.exists(old_image_path):
|
||||
os.remove(old_image_path)
|
||||
product.product_image = ProductService.save_image(file)
|
||||
|
||||
db.session.commit()
|
||||
return product
|
||||
|
||||
@staticmethod
|
||||
def delete_product(id):
|
||||
product = Product.query.get_or_404(id)
|
||||
|
||||
if product.product_image:
|
||||
image_path = os.path.join(current_app.config['UPLOAD_FOLDER'], product.product_image.split('/')[-1])
|
||||
if os.path.exists(image_path):
|
||||
os.remove(image_path)
|
||||
|
||||
db.session.delete(product)
|
||||
db.session.commit()
|
||||
return True
|
||||
|
||||
class TransactionService:
|
||||
@staticmethod
|
||||
def create_transaction(machine_id, product_name, quantity, amount, payment_type, status='Success'):
|
||||
"""Create a new transaction record"""
|
||||
transaction_id = f"TXN-{uuid.uuid4().hex[:8].upper()}"
|
||||
|
||||
new_transaction = Transaction(
|
||||
transaction_id=transaction_id,
|
||||
machine_id=machine_id,
|
||||
product_name=product_name,
|
||||
quantity=quantity,
|
||||
amount=amount,
|
||||
payment_type=payment_type,
|
||||
status=status
|
||||
)
|
||||
|
||||
db.session.add(new_transaction)
|
||||
db.session.commit()
|
||||
return new_transaction
|
||||
|
||||
@staticmethod
|
||||
def update_transaction_status(transaction_id, status, amount_receiving_status=None, return_amount=None):
|
||||
"""Update transaction status after dispensing"""
|
||||
transaction = Transaction.query.filter_by(transaction_id=transaction_id).first()
|
||||
if transaction:
|
||||
transaction.status = status
|
||||
if amount_receiving_status:
|
||||
transaction.amount_receiving_status = amount_receiving_status
|
||||
if return_amount:
|
||||
transaction.return_amount = return_amount
|
||||
db.session.commit()
|
||||
return transaction
|
||||
|
||||
@staticmethod
|
||||
def get_all_transactions(filters=None):
|
||||
"""Get all transactions with optional filters"""
|
||||
query = Transaction.query
|
||||
|
||||
if filters:
|
||||
if 'machine_id' in filters:
|
||||
query = query.filter_by(machine_id=filters['machine_id'])
|
||||
if 'status' in filters:
|
||||
query = query.filter_by(status=filters['status'])
|
||||
if 'date_from' in filters:
|
||||
query = query.filter(Transaction.created_at >= filters['date_from'])
|
||||
if 'date_to' in filters:
|
||||
query = query.filter(Transaction.created_at <= filters['date_to'])
|
||||
|
||||
return query.order_by(Transaction.created_at.desc()).all()
|
||||
|
||||
@staticmethod
|
||||
def get_transaction_by_id(transaction_id):
|
||||
"""Get single transaction by ID"""
|
||||
return Transaction.query.filter_by(transaction_id=transaction_id).first()
|
||||
|
||||
class RoleService:
|
||||
|
||||
@staticmethod
|
||||
def get_all_roles():
|
||||
"""Get all roles"""
|
||||
return Role.query.all()
|
||||
|
||||
@staticmethod
|
||||
def get_role_by_id(role_id):
|
||||
"""Get role by ID"""
|
||||
return Role.query.get_or_404(role_id)
|
||||
|
||||
@staticmethod
|
||||
def create_role(data):
|
||||
"""Create new role"""
|
||||
name = data.get('name')
|
||||
description = data.get('description', '')
|
||||
permissions = data.get('permissions', [])
|
||||
|
||||
# Check if role already exists
|
||||
existing_role = Role.query.filter_by(name=name).first()
|
||||
if existing_role:
|
||||
raise ValueError(f"Role '{name}' already exists")
|
||||
|
||||
new_role = Role(
|
||||
name=name,
|
||||
description=description,
|
||||
permissions=json.dumps(permissions)
|
||||
)
|
||||
|
||||
db.session.add(new_role)
|
||||
db.session.commit()
|
||||
|
||||
return new_role
|
||||
|
||||
@staticmethod
|
||||
def update_role(role_id, data):
|
||||
"""Update existing role"""
|
||||
role = Role.query.get_or_404(role_id)
|
||||
|
||||
if 'name' in data:
|
||||
# Check if new name conflicts with existing role
|
||||
existing = Role.query.filter(Role.name == data['name'], Role.id != role_id).first()
|
||||
if existing:
|
||||
raise ValueError(f"Role '{data['name']}' already exists")
|
||||
role.name = data['name']
|
||||
|
||||
if 'description' in data:
|
||||
role.description = data['description']
|
||||
|
||||
if 'permissions' in data:
|
||||
role.permissions = json.dumps(data['permissions'])
|
||||
|
||||
role.updated_at = datetime.datetime.utcnow()
|
||||
db.session.commit()
|
||||
|
||||
return role
|
||||
|
||||
@staticmethod
|
||||
def delete_role(role_id):
|
||||
"""Delete role"""
|
||||
role = Role.query.get_or_404(role_id)
|
||||
|
||||
# Check if any users have this role
|
||||
users_with_role = User.query.filter_by(roles=role.name).count()
|
||||
if users_with_role > 0:
|
||||
raise ValueError(f"Cannot delete role '{role.name}' - {users_with_role} user(s) still assigned to it")
|
||||
|
||||
db.session.delete(role)
|
||||
db.session.commit()
|
||||
|
||||
return True
|
||||
160
Machine-Backend/app/sms_notifications.json
Normal file
@ -0,0 +1,160 @@
|
||||
[
|
||||
{
|
||||
"timestamp": "2025-05-05 15:18:35",
|
||||
"username": "Mukesh",
|
||||
"email": "smukesh1304@gmail.com",
|
||||
"contact": "9080063331",
|
||||
"password": "B4DC37B8",
|
||||
"message": "Your account credentials: Username: Mukesh, Email: smukesh1304@gmail.com, Password: B4DC37B8"
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-05-05 18:31:21",
|
||||
"username": "Gokul",
|
||||
"email": "fate.altre@gmail.com",
|
||||
"contact": "9080063331",
|
||||
"password": "7CB34107",
|
||||
"message": "Your account credentials: Username: Gokul, Email: fate.altre@gmail.com, Password: 7CB34107"
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-05-05 20:53:59",
|
||||
"username": "Mukesh",
|
||||
"email": "smukesh1304@gmail.com",
|
||||
"contact": "9080063331",
|
||||
"password": "0A96ABB6",
|
||||
"message": "Your account credentials: Username: Mukesh, Email: smukesh1304@gmail.com, Password: 0A96ABB6"
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-05-05 20:54:21",
|
||||
"username": "Joe Done",
|
||||
"email": "fate.altre@gmail.com",
|
||||
"contact": "9080063331",
|
||||
"password": "1385EE14",
|
||||
"message": "Your account credentials: Username: Joe Done, Email: fate.altre@gmail.com, Password: 1385EE14"
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-05-05 21:26:23",
|
||||
"username": "Mukesh",
|
||||
"email": "smukesh1304@gmail.com",
|
||||
"contact": "9080063331",
|
||||
"password": "A5FAED83",
|
||||
"message": "Your account credentials: Username: Mukesh, Email: smukesh1304@gmail.com, Password: A5FAED83"
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-24 12:51:15",
|
||||
"username": "admin",
|
||||
"email": "fate.altre@gmail.com",
|
||||
"contact": "9080063331",
|
||||
"password": "D1FCD9E5",
|
||||
"message": "Your account credentials: Username: admin, Email: fate.altre@gmail.com, Password: D1FCD9E5"
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-24 13:19:32",
|
||||
"username": "testuser",
|
||||
"email": "test@example.com",
|
||||
"contact": "1234567890",
|
||||
"password": "67CC0B4F",
|
||||
"message": "Your account credentials: Username: testuser, Email: test@example.com, Password: 67CC0B4F"
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-24 14:40:29",
|
||||
"username": "mukesh",
|
||||
"email": "smukesh1304@gmail.com",
|
||||
"contact": "9080063331",
|
||||
"password": "5A353979",
|
||||
"message": "Your account credentials: Username: mukesh, Email: smukesh1304@gmail.com, Password: 5A353979"
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-24 14:46:06",
|
||||
"username": "mukesh",
|
||||
"email": "smukesh1304@gmail.com",
|
||||
"contact": "9080063331",
|
||||
"password": "0C42E059",
|
||||
"message": "Your account credentials: Username: mukesh, Email: smukesh1304@gmail.com, Password: 0C42E059"
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-19 16:37:59",
|
||||
"username": "mukesh",
|
||||
"email": "test@example.com",
|
||||
"contact": "9999999999",
|
||||
"password": "42F71FD9",
|
||||
"message": "Your account credentials: Username: mukesh, Email: test@example.com, Password: 42F71FD9"
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-19 16:38:01",
|
||||
"username": "mukesh",
|
||||
"email": "test@example.com",
|
||||
"contact": "9999999999",
|
||||
"password": "F0DE0F08",
|
||||
"message": "Your account credentials: Username: mukesh, Email: test@example.com, Password: F0DE0F08"
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-09-22 14:57:01",
|
||||
"username": "John",
|
||||
"email": "john@test.com",
|
||||
"contact": "9876543210",
|
||||
"password": "623EBBCB",
|
||||
"message": "Your account credentials: Username: John, Email: john@test.com, Password: 623EBBCB"
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-09-30 11:03:10",
|
||||
"username": "Rootxvending",
|
||||
"email": "praveenkumar@rootxvending.com",
|
||||
"contact": "9360857385",
|
||||
"password": "9213F482",
|
||||
"message": "Your account credentials: Username: Rootxvending, Email: praveenkumar@rootxvending.com, Password: 9213F482"
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-09-30 13:17:02",
|
||||
"user_id": "USR-MUKEMUKE8421",
|
||||
"username": "Mukesh S",
|
||||
"email": "mukesh@rtx.com",
|
||||
"contact": "9876543210",
|
||||
"password": "840F9104",
|
||||
"message": "Your account credentials: User ID: USR-MUKEMUKE8421, Username: Mukesh S, Email: mukesh@rtx.com, Password: 840F9104"
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-09-30 13:19:40",
|
||||
"user_id": "USR-MUKEMUKE8578",
|
||||
"username": "Mukesh S",
|
||||
"email": "mukesh@rxw.com",
|
||||
"contact": "9876543210",
|
||||
"password": "8382FB34",
|
||||
"message": "Your account credentials: User ID: USR-MUKEMUKE8578, Username: Mukesh S, Email: mukesh@rxw.com, Password: 8382FB34"
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-09-30 17:40:58",
|
||||
"user_id": "USR-PRAVPRAV4256",
|
||||
"username": "Praveen",
|
||||
"email": "praveen@rtx.com",
|
||||
"contact": "9876543210",
|
||||
"password": "F8CB73CB",
|
||||
"message": "Your account credentials: User ID: USR-PRAVPRAV4256, Username: Praveen, Email: praveen@rtx.com, Password: F8CB73CB"
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-09-30 20:17:34",
|
||||
"user_id": "USR-MUKEMUKE3650",
|
||||
"username": "Mukesh",
|
||||
"email": "mukesh@rootxwire.com",
|
||||
"contact": "9876543211",
|
||||
"password": "77C1C095",
|
||||
"message": "Your account credentials: User ID: USR-MUKEMUKE3650, Username: Mukesh, Email: mukesh@rootxwire.com, Password: 77C1C095"
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-09-30 20:22:22",
|
||||
"user_id": "USR-MUKEMUKE3938",
|
||||
"username": "Mukesh",
|
||||
"email": "mukesh@rootxwire.com",
|
||||
"contact": "9876543210",
|
||||
"password": "0358342E",
|
||||
"message": "Your account credentials: User ID: USR-MUKEMUKE3938, Username: Mukesh, Email: mukesh@rootxwire.com, Password: 0358342E"
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-09-30 20:25:49",
|
||||
"user_id": "USR-MUKEMUKE4145",
|
||||
"username": "mukesh",
|
||||
"email": "mukesh@rootxwire.com",
|
||||
"contact": "9080063331",
|
||||
"password": "E95284C4",
|
||||
"message": "Your account credentials: User ID: USR-MUKEMUKE4145, Username: mukesh, Email: mukesh@rootxwire.com, Password: E95284C4"
|
||||
}
|
||||
]
|
||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 238 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 353 KiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 353 KiB |
|
After Width: | Height: | Size: 353 KiB |
|
After Width: | Height: | Size: 238 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 238 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 353 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 353 KiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
BIN
Machine-Backend/instance/machines.db
Normal file
BIN
Machine-Backend/instance/vending.db
Normal file
5
Machine-Backend/requirements.txt
Normal file
@ -0,0 +1,5 @@
|
||||
Flask
|
||||
Flask-CORS
|
||||
Flask-SQLAlchemy
|
||||
python-dotenv
|
||||
pymysql
|
||||
5
Machine-Backend/run.py
Normal file
@ -0,0 +1,5 @@
|
||||
from app import create_app
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = create_app()
|
||||
app.run(debug=True, port=5000)
|
||||