Spaces:
Running
Running
# 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 --- | |
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 | |
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) | |
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 "✅" | |
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 | |
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 | |
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 | |
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"}) | |
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 | |
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) | |
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 | |
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 | |
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 | |
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 | |
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"}) | |
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 |