akiko19191's picture
Upload folder using huggingface_hub
dfe4909 verified
# 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():
# --- OPTIMIZATION: Use projection to exclude large binary image data ---
# We only need to know if 'image_data' exists, not its content. This dramatically
# reduces the data transferred from MongoDB to the Flask app, speeding up the response.
products_cursor = mongo.db.products.find({}, {'image_data': 0})
products_list = []
# Re-fetch the documents that have image_data to check for existence
# This is a bit of a workaround because we can't check for existence with projection easily
# A better long-term solution would be a boolean field like `has_binary_image`.
# For now, we'll check based on the projected data. If a product *might* have an image, we can assume it does.
# The logic below is slightly adjusted. A product document will still have the key 'image_data' if it was not projected out.
# The previous code was fine, but this makes it explicit that we are avoiding the large field.
products_cursor = mongo.db.products.find(
{},
# Exclude the large image_data field from the initial query
{'image_data': 0, 'image_content_type': 0}
)
# Get a set of IDs for products that DO have binary image data in a separate, fast query
products_with_images = {
str(p['_id']) for p in mongo.db.products.find(
{'image_data': {'$exists': True, '$ne': None}},
{'_id': 1} # Only fetch the ID
)
}
products_list = []
for p in products_cursor:
product_id_str = str(p['_id'])
image_url = None
# Check against our pre-fetched set of IDs
if product_id_str in products_with_images:
image_url = url_for('api.get_product_image', product_id=product_id_str, _external=True)
# Fallback to the stored URL
elif p.get('image_url'):
image_url = p.get('image_url')
products_list.append({
'id': product_id_str,
'name': p.get('name'),
'category': p.get('category'),
'modes': p.get('modes'),
'image_url': image_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':
# --- OPTIMIZATION: Use MongoDB Aggregation Pipeline to fetch cart and products in one go ---
pipeline = [
# 1. Find the user's cart
{'$match': {'user_email': user_email}},
# 2. Deconstruct the items array to process each item
{'$unwind': '$items'},
# 3. Convert string productId to ObjectId for lookup
{'$addFields': {'productId_obj': {'$toObjectId': '$items.productId'}}},
# 4. Join with the products collection ($lookup is like a JOIN)
{
'$lookup': {
'from': 'products',
'localField': 'productId_obj',
'foreignField': '_id',
'as': 'productDetails'
}
},
# 5. Deconstruct the resulting productDetails array (it will have 1 element)
{'$unwind': '$productDetails'},
# 6. Re-shape the document to match the frontend's expected format
{
'$project': {
'_id': 0,
'deliveryDate': '$deliveryDate',
'item': {
'quantity': '$items.quantity',
'mode': '$items.mode',
'product': {
'id': {'$toString': '$productDetails._id'},
'name': '$productDetails.name',
'modes': '$productDetails.modes',
'price': {'$getField': {'field': '$items.mode', 'input': '$productDetails.modes.price'}},
'image_url': {
'$cond': {
'if': {'$and': [
{'$ne': ['$productDetails.image_data', None]},
{'$ne': ['$productDetails.image_data', ""]}
]},
'then': url_for('api.get_product_image', product_id=str(ObjectId()), _external=True).replace(str(ObjectId()),""), # Placeholder for url construction
'else': '$productDetails.image_url'
}
}
}
}
}
},
# 7. Dynamically construct the image URL
{
'$addFields': {
"item.product.image_url": {
'$cond': {
'if': {'$ne': ["$item.product.image_url", None]},
'then': {
'$concat': [
request.host_url.rstrip('/'),
'/api/product_image/',
"$item.product.id"
]
},
'else': None
}
}
}
},
# 8. Group all items back into a single cart document
{
'$group': {
'_id': '$_id',
'deliveryDate': {'$first': '$deliveryDate'},
'items': {'$push': '$item'}
}
}
]
result = list(mongo.db.carts.aggregate(pipeline))
if not result:
return jsonify({'items': [], 'deliveryDate': None})
return jsonify(result[0])
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_cursor = mongo.db.orders.find({'user_email': user_email}).sort('created_at', -1)
user_orders = list(user_orders_cursor)
if not user_orders: return jsonify([])
# Fetch status from DB, or from Zoho if not present in DB.
for order in user_orders:
# If status is present in our DB, use it and skip the API call.
if 'zoho_status' in order:
order['status'] = order['zoho_status']
continue
# If status is not in DB, fetch from Zoho, update DB, and then use it.
live_status = 'pending'
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_api_status = invoice.get('status')
if zoho_api_status == 'draft':
live_status = 'pending'
elif zoho_api_status == 'sent':
live_status = 'Processing'
elif zoho_api_status == 'paid':
live_status = 'Completed'
elif zoho_api_status == 'void':
live_status = 'cancelled'
# Save the newly fetched status to MongoDB for future requests
mongo.db.orders.update_one(
{'_id': order['_id']},
{'$set': {'zoho_status': live_status}}
)
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
# --- OPTIMIZATION: Use a single aggregation to populate product details for all orders ---
pipeline = [
{'$match': {'user_email': user_email}},
{'$sort': {'created_at': -1}},
{'$unwind': '$items'},
{'$addFields': {'productId_obj': {'$toObjectId': '$items.productId'}}},
{
'$lookup': {
'from': 'products',
'localField': 'productId_obj',
'foreignField': '_id',
'as': 'productDetails'
}
},
{'$unwind': '$productDetails'},
{
'$group': {
'_id': '$_id',
'items': {'$push': {
'quantity': '$items.quantity',
'mode': '$items.mode',
'price': {'$let': {'vars': {'mode_details': {'$getField': {'field': '$items.mode', 'input': '$productDetails.modes'}}}, 'in': '$$mode_details.price'}},
'product': {
'id': {'$toString': '$productDetails._id'},
'name': '$productDetails.name',
'modes': '$productDetails.modes',
'image_url': {
'$cond': {
'if': {'$ifNull': ['$productDetails.image_data', False]},
'then': {'$concat': [request.host_url.rstrip('/'), '/api/product_image/', {'$toString': '$productDetails._id'}]},
'else': '$productDetails.image_url'
}
}
}
}},
# Carry over all original order fields
'doc': {'$first': '$$ROOT'}
}
},
{
'$replaceRoot': {
'newRoot': {
'$mergeObjects': ['$doc', {'items': '$items'}]
}
}
},
{'$sort': {'created_at': -1}}
]
populated_orders = list(mongo.db.orders.aggregate(pipeline))
# Merge the live status back into the populated orders
status_map = {str(order['_id']): order['status'] for order in user_orders}
for order in populated_orders:
order_id_str = str(order['_id'])
order['status'] = status_map.get(order_id_str, 'pending')
order['_id'] = order_id_str # Convert ObjectId to string for JSON
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(populated_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