akiko19191's picture
Upload folder using huggingface_hub
06799f0 verified
raw
history blame
24.9 kB
# api.py
from flask import Blueprint, request, jsonify, current_app, redirect, Response, url_for
from bson.objectid import ObjectId
from datetime import datetime
from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity
from .extensions import bcrypt, mongo
from .xero_client import make_zoho_api_request
from .xero_utils import trigger_invoice_creation, trigger_contact_creation,sync_user_approval_from_zoho
from .email_utils import send_order_confirmation_email, send_registration_email, send_registration_admin_notification, send_login_notification_email, send_cart_reminder_email
from .general_utils import get_next_order_serial
api_bp = Blueprint('api', __name__)
# --- NEW: Endpoint to serve images stored in MongoDB ---
@api_bp.route('/product_image/<product_id>')
def get_product_image(product_id):
"""
Fetches image data stored as binary in the products collection.
"""
try:
product = mongo.db.products.find_one(
{'_id': ObjectId(product_id)},
{'image_data': 1, 'image_content_type': 1} # Projection to get only needed fields
)
if product and 'image_data' in product and product['image_data'] is not None:
# Clean the content_type string to ensure it's a valid MIME type for browsers.
content_type = product.get('image_content_type', 'image/jpeg')
mime_type = content_type.split(';')[0].strip()
# Serve the binary data with the correct mimetype
return Response(product['image_data'], mimetype=mime_type)
else:
# Return a 404 Not Found if the product or its image data doesn't exist
return jsonify({"msg": "Image not found"}), 404
except Exception as e:
current_app.logger.error(f"Error serving image for product_id {product_id}: {e}")
return jsonify({"msg": "Error serving image"}), 500
@api_bp.route('/products', methods=['GET'])
def get_products():
# --- MODIFIED: Construct the correct image_url pointing to our new endpoint ---
products_cursor = mongo.db.products.find()
products_list = []
for p in products_cursor:
image_url = None
# If image_data exists, create a URL to our new endpoint
if p.get('image_data'):
# FIX: Generate an absolute URL to avoid cross-origin issues with the frontend dev server.
image_url = url_for('api.get_product_image', product_id=str(p['_id']), _external=True)
# Otherwise, use the fallback AI-generated URL if it exists
elif p.get('image_url'):
image_url = p.get('image_url')
products_list.append({
'id': str(p['_id']),
'name': p.get('name'),
'category': p.get('category'),
'modes': p.get('modes'),
'image_url': image_url, # This will be the correct, usable URL
'description': p.get('description', '')
})
return jsonify(products_list)
# ... (The rest of your api.py file remains unchanged)
@api_bp.route('/sync_xero_users', methods=['GET'])
def sync_xero_users():
sync_user_approval_from_zoho()
return "✅"
# @api_bp.route('/clear')
# def clear_all():
# mongo.db.users.delete_many({})
# mongo.db.orders.delete_many({})
# return "✅"
@api_bp.route('/register', methods=['POST'])
def register():
data = request.get_json()
email = data.get('email')
password = data.get('password')
company_name = data.get('businessName')
if not all([email, password, company_name]):
return jsonify({"msg": "Missing required fields: Email, Password, and Business Name"}), 400
if mongo.db.users.find_one({'email': email}):
return jsonify({"msg": "A user with this email already exists"}), 409
hashed_password = bcrypt.generate_password_hash(password).decode('utf-8')
user_document = data.copy()
user_document['password'] = hashed_password
user_document['company_name'] = company_name
user_document['is_approved'] = False
user_document['is_admin'] = False
mongo.db.users.insert_one(user_document)
trigger_contact_creation(data)
try:
send_registration_email(data)
send_registration_admin_notification(data) # Send notification to admin
except Exception as e:
current_app.logger.error(f"Failed to send registration emails for {email}: {e}")
return jsonify({"msg": "Registration successful! Your application is being processed."}), 201
@api_bp.route('/login', methods=['POST'])
def login():
data = request.get_json()
email, password = data.get('email'), data.get('password')
user = mongo.db.users.find_one({'email': email})
if user and user.get('password') and bcrypt.check_password_hash(user['password'], password):
if not user.get('is_approved', False): return jsonify({"msg": "Account pending approval"}), 403
try:
send_login_notification_email(user)
except Exception as e:
current_app.logger.error(f"Failed to send login notification email to {email}: {e}")
access_token = create_access_token(identity=email)
return jsonify(access_token=access_token, email=user['email'], companyName=user['businessName'],contactPerson=user.get('contactPerson', '')) , 200
return jsonify({"msg": "Bad email or password"}), 401
@api_bp.route('/profile', methods=['GET'])
@jwt_required()
def get_user_profile():
user_email = get_jwt_identity()
user = mongo.db.users.find_one({'email': user_email})
if not user:
return jsonify({"msg": "User not found"}), 404
profile_data = {
'deliveryAddress': user.get('businessAddress', ''),
'mobileNumber': user.get('phoneNumber', '')
}
return jsonify(profile_data), 200
@api_bp.route('/cart', methods=['GET', 'POST'])
@jwt_required()
def handle_cart():
user_email = get_jwt_identity()
if request.method == 'GET':
cart = mongo.db.carts.find_one({'user_email': user_email})
if not cart:
return jsonify({'items': [], 'deliveryDate': None})
populated_items = []
if cart.get('items'):
product_ids = [ObjectId(item['productId']) for item in cart['items']]
if product_ids:
products_cursor = mongo.db.products.find({'_id': {'$in': product_ids}})
products = {str(p['_id']): p for p in products_cursor}
for item in cart['items']:
details = products.get(item['productId'])
if details:
mode = item.get('mode', 'piece')
mode_details = details.get('modes', {}).get(mode)
if mode_details:
price = mode_details.get('price')
image_url = None
if details.get('image_data'):
# FIX: Generate an absolute URL.
image_url = url_for('api.get_product_image', product_id=str(details['_id']), _external=True)
elif details.get('image_url'):
image_url = details.get('image_url')
populated_items.append({
'product': {
'id': str(details['_id']),
'name': details.get('name'),
'modes': details.get('modes'),
'image_url': image_url,
'price': price
},
'quantity': item['quantity'],
'mode': mode
})
return jsonify({
'items': populated_items,
'deliveryDate': cart.get('deliveryDate')
})
if request.method == 'POST':
data = request.get_json()
update_doc = {
'user_email': user_email,
'updated_at': datetime.utcnow()
}
if 'items' in data:
sanitized_items = []
for item in data['items']:
try:
if not all(k in item for k in ['productId', 'quantity', 'mode']) or item['quantity'] is None:
continue
mode = item.get('mode')
quantity = item.get('quantity')
if mode == 'weight':
numeric_quantity = float(quantity)
else:
numeric_quantity = int(float(quantity))
if numeric_quantity < 0:
continue
sanitized_items.append({
'productId': item['productId'],
'quantity': numeric_quantity,
'mode': mode
})
except (ValueError, TypeError):
return jsonify({"msg": f"Invalid quantity format for item."}), 400
update_doc['items'] = sanitized_items
if 'deliveryDate' in data:
update_doc['deliveryDate'] = data['deliveryDate']
mongo.db.carts.update_one(
{'user_email': user_email},
{'$set': update_doc},
upsert=True
)
return jsonify({"msg": "Cart updated successfully"})
@api_bp.route('/orders/<serial_no>/download_invoice', methods=['GET'])
@jwt_required()
def download_invoice(serial_no):
user_email = get_jwt_identity()
order = mongo.db.orders.find_one({'serial_no': int(serial_no), 'user_email': user_email})
if not order:
return jsonify({"msg": "Order not found or access denied"}), 404
try:
invoices_response, _ = make_zoho_api_request('GET', '/invoices', params={'reference_number': serial_no})
if not invoices_response or not invoices_response.get('invoices'):
return jsonify({"msg": "Invoice not found in our billing system."}), 404
invoice_id = invoices_response['invoices'][0].get('invoice_id')
if not invoice_id:
return jsonify({"msg": "Could not identify the invoice in our billing system."}), 404
pdf_content, headers = make_zoho_api_request('GET', f'/invoices/{invoice_id}', params={'accept': 'pdf'})
if not pdf_content:
return jsonify({"msg": "Failed to download the invoice PDF from our billing system."}), 500
return Response(
pdf_content,
mimetype='application/pdf',
headers={
"Content-Disposition": f"attachment; filename=invoice-{serial_no}.pdf",
"Content-Type": "application/pdf"
}
)
except Exception as e:
current_app.logger.error(f"Error downloading invoice {serial_no} from Zoho: {e}")
return jsonify({"msg": "An internal error occurred while fetching the invoice."}), 500
@api_bp.route('/orders', methods=['GET', 'POST'])
@jwt_required()
def handle_orders():
user_email = get_jwt_identity()
if request.method == 'POST':
cart = mongo.db.carts.find_one({'user_email': user_email})
if not cart or not cart.get('items'): return jsonify({"msg": "Your cart is empty"}), 400
data = request.get_json()
if not all([data.get('deliveryDate'), data.get('deliveryAddress'), data.get('mobileNumber')]): return jsonify({"msg": "Missing delivery information"}), 400
user = mongo.db.users.find_one({'email': user_email})
if not user:
return jsonify({"msg": "User not found"}), 404
order_doc = {
'user_email': user_email, 'items': cart['items'], 'delivery_date': data['deliveryDate'],
'delivery_address': data['deliveryAddress'], 'mobile_number': data['mobileNumber'],
'additional_info': data.get('additionalInfo'), 'total_amount': data.get('totalAmount'),
'status': 'pending', 'created_at': datetime.utcnow()
}
order_doc['serial_no'] = get_next_order_serial()
order_id = mongo.db.orders.insert_one(order_doc).inserted_id
order_doc['_id'] = order_id
order_details_for_xero = {
"order_id": order_doc['serial_no'], "user_email": user_email, "items": cart['items'],
"delivery_address": data['deliveryAddress'], "mobile_number": data['mobileNumber'],"deliverydate":data["deliveryDate"],'additional_info': data.get('additionalInfo')
}
trigger_invoice_creation(order_details_for_xero)
try:
product_ids = [ObjectId(item['productId']) for item in cart['items']]
products_map = {str(p['_id']): p for p in mongo.db.products.find({'_id': {'$in': product_ids}})}
order_doc['populated_items'] = [{
"name": products_map.get(item['productId'], {}).get('name', 'N/A'),
"quantity": item['quantity'],
"mode": item.get('mode', 'pieces')
} for item in cart['items']]
send_order_confirmation_email(order_doc, user)
except Exception as e:
current_app.logger.error(f"Failed to send confirmation email for order {order_id}: {e}")
mongo.db.carts.delete_one({'user_email': user_email})
return jsonify({"msg": "Order placed successfully! You will be redirected shortly to the Orders Page!", "orderId": str(order_id)}), 201
if request.method == 'GET':
user_orders = list(mongo.db.orders.find({'user_email': user_email}).sort('created_at', -1))
if not user_orders: return jsonify([])
all_product_ids = {ObjectId(item['productId']) for order in user_orders for item in order.get('items', [])}
products_cursor = mongo.db.products.find({'_id': {'$in': list(all_product_ids)}})
products = {str(p['_id']): p for p in products_cursor}
for order in user_orders:
# Determine status from Zoho
live_status = 'pending' # Default status
try:
serial_no = order.get('serial_no')
if serial_no:
invoices_response, _ = make_zoho_api_request('GET', '/invoices', params={'reference_number': serial_no})
if invoices_response and invoices_response.get('invoices'):
invoice = invoices_response['invoices'][0]
zoho_status = invoice.get('status')
if zoho_status == 'draft':
live_status = 'pending'
elif zoho_status == 'sent':
live_status = 'Processing'
elif zoho_status == 'paid':
live_status = 'Completed'
elif zoho_status == 'void':
live_status = 'cancelled'
except Exception as e:
current_app.logger.error(f"Could not fetch Zoho invoice status for order {order.get('serial_no')}: {e}")
order['status'] = live_status
# Populate items
populated_items = []
for item in order.get('items', []):
p = products.get(item['productId'])
if p:
mode = item.get('mode', 'pieces')
mode_details = p.get('modes', {}).get(mode, {})
image_url = None
if p.get('image_data'):
image_url = url_for('api.get_product_image', product_id=str(p['_id']), _external=True)
elif p.get('image_url'):
image_url = p.get('image_url')
populated_items.append({
'quantity': item['quantity'],
'mode': mode,
'price': mode_details.get('price'),
'product': {
'id': str(p['_id']),
'name': p.get('name'),
'modes': p.get('modes'),
'image_url': image_url
}
})
order['items'] = populated_items
order['_id'] = str(order['_id'])
order['created_at'] = order['created_at'].isoformat()
order['delivery_date'] = order['delivery_date'] if isinstance(order['delivery_date'], str) else order['delivery_date'].isoformat()
return jsonify(user_orders)
@api_bp.route('/orders/<order_id>', methods=['GET'])
@jwt_required()
def get_order(order_id):
user_email = get_jwt_identity()
try:
order = mongo.db.orders.find_one({'_id': ObjectId(order_id), 'user_email': user_email})
if not order:
return jsonify({"msg": "Order not found or access denied"}), 404
order['_id'] = str(order['_id'])
return jsonify(order), 200
except Exception as e:
return jsonify({"msg": f"Invalid Order ID format: {e}"}), 400
@api_bp.route('/orders/<order_id>', methods=['PUT'])
@jwt_required()
def update_order(order_id):
user_email = get_jwt_identity()
order = mongo.db.orders.find_one({'_id': ObjectId(order_id), 'user_email': user_email})
if not order:
return jsonify({"msg": "Order not found or access denied"}), 404
if order.get('status') not in ['pending', 'confirmed']:
return jsonify({"msg": f"Order with status '{order.get('status')}' cannot be modified."}), 400
cart = mongo.db.carts.find_one({'user_email': user_email})
if not cart or not cart.get('items'):
return jsonify({"msg": "Cannot update with an empty cart. Please add items."}), 400
data = request.get_json()
update_doc = {
'items': cart['items'],
'delivery_date': data['deliveryDate'],
'delivery_address': data['deliveryAddress'],
'mobile_number': data['mobileNumber'],
'additional_info': data.get('additionalInfo'),
'total_amount': data.get('totalAmount'),
'updated_at': datetime.utcnow()
}
mongo.db.orders.update_one({'_id': ObjectId(order_id)}, {'$set': update_doc})
mongo.db.carts.delete_one({'user_email': user_email})
return jsonify({"msg": "Order updated successfully!", "orderId": order_id}), 200
@api_bp.route('/orders/<order_id>/cancel', methods=['POST'])
@jwt_required()
def cancel_order(order_id):
user_email = get_jwt_identity()
order = mongo.db.orders.find_one({'_id': ObjectId(order_id), 'user_email': user_email})
if not order:
return jsonify({"msg": "Order not found or access denied"}), 404
serial_no = order.get('serial_no')
if not serial_no:
return jsonify({"msg": "Cannot cancel order without a billing reference."}), 400
try:
# Find the corresponding invoice in Zoho
invoices_response, _ = make_zoho_api_request('GET', '/invoices', params={'reference_number': serial_no})
if not invoices_response or not invoices_response.get('invoices'):
return jsonify({"msg": "Invoice not found in our billing system. Cannot cancel."}), 404
invoice = invoices_response['invoices'][0]
invoice_id = invoice.get('invoice_id')
zoho_status = invoice.get('status')
# The order can only be cancelled if the invoice is a draft
if zoho_status != 'draft':
return jsonify({"msg": "This order cannot be cancelled as it is already being processed."}), 400
# Proceed to void the invoice in Zoho
void_response, _ = make_zoho_api_request('POST', f'/invoices/{invoice_id}/status/void')
if not void_response:
return jsonify({"msg": "Failed to cancel the order in the billing system."}), 500
# If Zoho void was successful, update our local DB status
mongo.db.orders.update_one(
{'_id': ObjectId(order_id)},
{'$set': {'status': 'cancelled', 'updated_at': datetime.utcnow()}}
)
return jsonify({"msg": "Order has been cancelled."}), 200
except Exception as e:
current_app.logger.error(f"Error cancelling order {order_id} and voiding Zoho invoice: {e}")
return jsonify({"msg": "An internal error occurred while cancelling the order."}), 500
@api_bp.route('/sendmail', methods=['GET'])
def send_cart_reminders():
try:
carts_with_items = list(mongo.db.carts.find({'items': {'$exists': True, '$ne': []}}))
if not carts_with_items:
return jsonify({"msg": "No users with pending items in cart."}), 200
user_emails = [cart['user_email'] for cart in carts_with_items]
all_product_ids = {
ObjectId(item['productId'])
for cart in carts_with_items
for item in cart.get('items', [])
}
users_cursor = mongo.db.users.find({'email': {'$in': user_emails}})
products_cursor = mongo.db.products.find({'_id': {'$in': list(all_product_ids)}})
users_map = {user['email']: user for user in users_cursor}
products_map = {str(prod['_id']): prod for prod in products_cursor}
emails_sent_count = 0
for cart in carts_with_items:
user = users_map.get(cart['user_email'])
if not user:
current_app.logger.warning(f"Cart found for non-existent user: {cart['user_email']}")
continue
populated_items = []
for item in cart.get('items', []):
product_details = products_map.get(item['productId'])
if product_details:
populated_items.append({
'product': {
'id': str(product_details['_id']),
'name': product_details.get('name'),
},
'quantity': item['quantity']
})
if populated_items:
try:
send_cart_reminder_email(user, populated_items)
emails_sent_count += 1
except Exception as e:
current_app.logger.error(f"Failed to send cart reminder to {user['email']}: {e}")
return jsonify({"msg": f"Cart reminder process finished. Emails sent to {emails_sent_count} users."}), 200
except Exception as e:
current_app.logger.error(f"Error in /sendmail endpoint: {e}")
return jsonify({"msg": "An internal error occurred while sending reminders."}), 500
@api_bp.route('/admin/users/approve/<user_id>', methods=['POST'])
@jwt_required()
def approve_user(user_id):
mongo.db.users.update_one({'_id': ObjectId(user_id)}, {'$set': {'is_approved': True}})
return jsonify({"msg": f"User {user_id} approved"})
@api_bp.route('/request-item', methods=['POST'])
@jwt_required()
def request_item():
user_email = get_jwt_identity()
data = request.get_json()
if not data or not data.get('details'):
return jsonify({"msg": "Item details are required."}), 400
details = data.get('details').strip()
if not details:
return jsonify({"msg": "Item details cannot be empty."}), 400
try:
user = mongo.db.users.find_one({'email': user_email}, {'company_name': 1})
company_name = user.get('company_name', 'N/A') if user else 'N/A'
request_doc = {
'user_email': user_email,
'company_name': company_name,
'details': details,
'status': 'new',
'requested_at': datetime.utcnow()
}
mongo.db.item_requests.insert_one(request_doc)
return jsonify({"msg": "Your item request has been submitted. We will look into it!"}), 201
except Exception as e:
current_app.logger.error(f"Error processing item request for {user_email}: {e}")
return jsonify({"msg": "An internal server error occurred."}), 500