akiko19191 commited on
Commit
ddd8005
·
verified ·
1 Parent(s): 953b8ab

Upload folder using huggingface_hub

Browse files
app/__init__.py CHANGED
@@ -1,11 +1,19 @@
1
  import logging
2
  from flask import Flask, jsonify
 
3
  from . import config
4
  from .extensions import mongo, bcrypt, jwt, cors, session, oauth
5
  from .xero_client import api_client, xero # <-- IMPORT XERO REMOTE APP
6
-
 
 
7
  def create_app():
 
8
  app = Flask(__name__)
 
 
 
 
9
  app.config.from_object(config)
10
 
11
  logging.basicConfig(level=logging.INFO)
 
1
  import logging
2
  from flask import Flask, jsonify
3
+
4
  from . import config
5
  from .extensions import mongo, bcrypt, jwt, cors, session, oauth
6
  from .xero_client import api_client, xero # <-- IMPORT XERO REMOTE APP
7
+ from .ai_features import ai_bp
8
+ from .page_features import page_bp
9
+ # app.register_blueprint(api_bp, url_prefix='/api')
10
  def create_app():
11
+
12
  app = Flask(__name__)
13
+
14
+
15
+ app.register_blueprint(ai_bp, url_prefix='/api')
16
+ app.register_blueprint(page_bp, url_prefix='/api/pages')
17
  app.config.from_object(config)
18
 
19
  logging.basicConfig(level=logging.INFO)
app/__pycache__/__init__.cpython-311.pyc CHANGED
Binary files a/app/__pycache__/__init__.cpython-311.pyc and b/app/__pycache__/__init__.cpython-311.pyc differ
 
app/__pycache__/ai_features.cpython-311.pyc ADDED
Binary file (36.4 kB). View file
 
app/__pycache__/api.cpython-311.pyc CHANGED
Binary files a/app/__pycache__/api.cpython-311.pyc and b/app/__pycache__/api.cpython-311.pyc differ
 
app/__pycache__/config.cpython-311.pyc CHANGED
Binary files a/app/__pycache__/config.cpython-311.pyc and b/app/__pycache__/config.cpython-311.pyc differ
 
app/__pycache__/page_features.cpython-311.pyc ADDED
Binary file (11.1 kB). View file
 
app/__pycache__/xero_client.cpython-311.pyc CHANGED
Binary files a/app/__pycache__/xero_client.cpython-311.pyc and b/app/__pycache__/xero_client.cpython-311.pyc differ
 
app/__pycache__/xero_routes.cpython-311.pyc CHANGED
Binary files a/app/__pycache__/xero_routes.cpython-311.pyc and b/app/__pycache__/xero_routes.cpython-311.pyc differ
 
app/__pycache__/xero_utils.cpython-311.pyc CHANGED
Binary files a/app/__pycache__/xero_utils.cpython-311.pyc and b/app/__pycache__/xero_utils.cpython-311.pyc differ
 
app/ai_features.py ADDED
@@ -0,0 +1,617 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # your_app/ai_features.py
2
+
3
+ from flask import Blueprint, request, jsonify, current_app
4
+ from bson.objectid import ObjectId
5
+ from datetime import datetime, date, timedelta
6
+ from flask_jwt_extended import jwt_required, get_jwt_identity
7
+ from .extensions import bcrypt, mongo
8
+ from .Company_Info import company_info
9
+
10
+ from google import genai
11
+ from google.genai import types
12
+ import os
13
+ import json
14
+
15
+ # +++ START: WHATSAPP FEATURE IMPORTS +++
16
+ import requests
17
+ from twilio.twiml.messaging_response import MessagingResponse
18
+ # +++ END: WHATSAPP FEATURE IMPORTS +++
19
+
20
+ ai_bp = Blueprint('ai', __name__)
21
+
22
+ today = date.today()
23
+ weekday_number = today.weekday()
24
+ days_of_week = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
25
+ weekday_name = days_of_week[weekday_number]
26
+
27
+ # --- NEW: Define functions for the AI model ---
28
+
29
+ add_items_to_cart_function = {
30
+ "name": "add_items_to_cart",
31
+ "description": "Extracts order details from a user's message to add products to their shopping cart. Can also set a delivery date.",
32
+ "parameters": {
33
+ "type": "object", "properties": {
34
+ "items": { "type": "array", "description": "A list of items to add to the cart.", "items": { "type": "object", "properties": { "product_name": { "type": "string", "description": "The name of the product. This must match one of the available product names.", }, "quantity": { "type": "number", "description": "The quantity of the product.", }, "unit": { "type": "string", "description": "The unit of measure for the quantity (e.g., 'lb', 'pieces', 'box', 'bunch'). This must match one of the available units for the product.", }, }, "required": ["product_name", "quantity", "unit"], }, },
35
+ "delivery_date": {
36
+ "type": "string",
37
+ "description": "The desired delivery date for the order, in YYYY-MM-DD format. The user must explicitly state a date.",
38
+ }
39
+ },
40
+ "required": ["items"],
41
+ },
42
+ }
43
+
44
+ # +++ START: NEW FUNCTION DEFINITIONS FOR CART MANAGEMENT +++
45
+ remove_items_from_cart_function = {
46
+ "name": "remove_items_from_cart",
47
+ "description": "Removes one or more specific items from the user's shopping cart based on their name.",
48
+ "parameters": {
49
+ "type": "object",
50
+ "properties": {
51
+ "items": {
52
+ "type": "array",
53
+ "description": "A list of items to remove from the cart. Only product_name is required.",
54
+ "items": {
55
+ "type": "object",
56
+ "properties": {
57
+ "product_name": {
58
+ "type": "string",
59
+ "description": "The name of the product to remove. This must match an available product name.",
60
+ },
61
+ },
62
+ "required": ["product_name"],
63
+ },
64
+ }
65
+ },
66
+ "required": ["items"],
67
+ },
68
+ }
69
+
70
+ clear_cart_function = {
71
+ "name": "clear_cart",
72
+ "description": "Removes all items from the user's shopping cart. Use when the user wants to start over or empty their cart.",
73
+ "parameters": {"type": "object", "properties": {}}
74
+ }
75
+ # +++ END: NEW FUNCTION DEFINITIONS FOR CART MANAGEMENT +++
76
+
77
+ navigate_to_page_function = {
78
+ "name": "navigate_to_page",
79
+ "description": "Provides a button in the chat to navigate the user to a specific page.",
80
+ "parameters": {
81
+ "type": "object",
82
+ "properties": {
83
+ "page": {
84
+ "type": "string",
85
+ "description": "The destination page. Must be one of the allowed pages.",
86
+ "enum": ["orders", "cart", "catalog", "home", "about", "care", "login", "register"]
87
+ }
88
+ },
89
+ "required": ["page"]
90
+ }
91
+ }
92
+
93
+ get_my_orders_function = {
94
+ "name": "get_my_orders",
95
+ "description": "Retrieves a summary of the user's most recent orders to display in the chat.",
96
+ "parameters": {"type": "object", "properties": {}}
97
+ }
98
+
99
+ get_cart_items_function = {
100
+ "name": "get_cart_items",
101
+ "description": "Retrieves the items currently in the user's shopping cart and lists them.",
102
+ "parameters": {"type": "object", "properties": {}}
103
+ }
104
+
105
+ get_order_details_function = {
106
+ "name": "get_order_details",
107
+ "description": "Retrieves the specific items and details for a single order based on its ID.",
108
+ "parameters": {
109
+ "type": "object",
110
+ "properties": {
111
+ "order_id": {
112
+ "type": "string",
113
+ "description": "The ID of the order to fetch. Can be the full ID or the last 6 characters."
114
+ }
115
+ },
116
+ "required": ["order_id"]
117
+ }
118
+ }
119
+
120
+ cancel_order_function = {
121
+ "name": "cancel_order",
122
+ "description": "Proposes to cancel an order based on its ID. This requires user confirmation.",
123
+ "parameters": {
124
+ "type": "object",
125
+ "properties": {
126
+ "order_id": {
127
+ "type": "string",
128
+ "description": "The ID of the order to be cancelled. Can be the full ID or the last 6 characters."
129
+ }
130
+ },
131
+ "required": ["order_id"]
132
+ }
133
+ }
134
+
135
+ # +++ START: NEW FUNCTION DEFINITION FOR PLACING AN ORDER +++
136
+ place_order_function = {
137
+ "name": "place_order",
138
+ "description": "Places a final order using the items in the user's cart. This action is final and will clear the cart.",
139
+ "parameters": {
140
+ "type": "object",
141
+ "properties": {
142
+ "delivery_date": {
143
+ "type": "string",
144
+ "description": "The desired delivery date in YYYY-MM-DD format. Must be confirmed with the user."
145
+ },
146
+ "additional_info": {
147
+ "type": "string",
148
+ "description": "Any special instructions or notes from the user for the delivery."
149
+ }
150
+ },
151
+ "required": ["delivery_date"]
152
+ }
153
+ }
154
+ # +++ END: NEW FUNCTION DEFINITION FOR PLACING AN ORDER +++
155
+
156
+ register_new_customer_function = {
157
+ "name": "register_new_customer",
158
+ "description": "Registers a new customer by collecting their essential details. Use this when a user expresses intent to sign up or is not recognized.",
159
+ "parameters": {
160
+ "type": "object",
161
+ "properties": {
162
+ "businessName": {
163
+ "type": "string",
164
+ "description": "The name of the customer's business or company."
165
+ },
166
+ "email": {
167
+ "type": "string",
168
+ "description": "The customer's primary email address. This will be their login username."
169
+ },
170
+ "password": {
171
+ "type": "string",
172
+ "description": "A password for the user's account. The user must provide this."
173
+ },
174
+ "phoneNumber": {
175
+ "type": "string",
176
+ "description": "The user's contact phone number."
177
+ },
178
+ "businessAddress": {
179
+ "type": "string",
180
+ "description": "The full delivery address for the business."
181
+ },
182
+ "contactPerson": {
183
+ "type": "string",
184
+ "description": "The customer's full name or primary contact person's name."
185
+ }
186
+ },
187
+ "required": ["businessName", "email", "password", "phoneNumber", "businessAddress"]
188
+ }
189
+ }
190
+
191
+
192
+ @ai_bp.route('/chat', methods=['POST'])
193
+ @jwt_required()
194
+ def handle_ai_chat():
195
+ # ... (Web chat endpoint remains unchanged)
196
+ user_email = get_jwt_identity()
197
+ MODES = ['case', 'bag', 'piece', 'tray', 'weight']
198
+ all_products = list(mongo.db.products.find({}, {'name': 1, 'unit': 1, '_id': 0}))
199
+ product_context_list = []
200
+ for p in all_products:
201
+ if 'name' not in p: continue
202
+ unit_string = p.get('unit', '').lower()
203
+ available_modes = [mode for mode in MODES if mode in unit_string]
204
+ if available_modes:
205
+ product_context_list.append(f"{p['name']} (available units: {', '.join(available_modes)})")
206
+ else:
207
+ product_context_list.append(p['name'])
208
+ client = genai.Client(api_key="AIzaSyB7yKIdfW7Umv62G47BCdJjoHTJ9TeiAko")
209
+
210
+ current_date = datetime.now().strftime('%Y-%m-%d')
211
+ min_delivery_date = (datetime.now() + timedelta(days=2)).strftime('%Y-%m-%d')
212
+
213
+ tools = types.Tool(function_declarations=[
214
+ add_items_to_cart_function,
215
+ navigate_to_page_function,
216
+ get_my_orders_function,
217
+ cancel_order_function,
218
+ register_new_customer_function
219
+ ])
220
+ config = types.GenerateContentConfig(tools=[tools])
221
+
222
+ instruction_prompt = f"""
223
+ You are an expert assistant for a food wholesale company. Your capabilities include:
224
+ 1. Ordering & Delivery Date: Use `add_items_to_cart` to add items to the user's cart. You can also set a delivery date if the user specifies one. The current date is {current_date,weekday_name}. The earliest possible delivery date is {min_delivery_date}. If the user asks for a date earlier than this, inform them of the constraint and ask them to provide a new valid date.
225
+ 2. Product List: {'; '.join(product_context_list)}.
226
+ 3. Page Navigation: Use `navigate_to_page` if the user wants to GO to a page like 'my orders', 'about us', 'customer care', 'login', etc.
227
+ 4. Order Summary: Use `get_my_orders` to LIST a summary of recent orders in the chat.
228
+ 5. Order Cancellation: Use `cancel_order` to start the cancellation process for a SPECIFIC order. The user must provide an order ID.
229
+ 6. New Customer Registration: Use `register_new_customer` if a user wants to sign up.
230
+ 7. The Capability You must use AT ALL TIMES: Luring the customer to place an order and persuading them to buy more products.
231
+ *Always Brag about the quality of our products and our customer service and try to persuade the customer to place an order.*
232
+
233
+
234
+ Company Info for general questions: {company_info}
235
+ *** IMPORTANT: When providing a text-based answer, YOU MUST USE MARKDOWN. Use headings, bold text, bullet points (`* item`), and other formatting to make your answers clear, structured, and easy to read. ***
236
+ Always refer to the chat history for context. **If information is missing, ask for it.**
237
+ """
238
+
239
+ user_input_part, history_data = None, []
240
+ if 'multipart/form-data' in request.content_type:
241
+ audio_file = request.files.get('audio')
242
+ history_json = request.form.get('history', '[]')
243
+ history_data = json.loads(history_json)
244
+ user_input_part = types.Part.from_bytes(data=audio_file.read(), mime_type=audio_file.mimetype.split(';')[0])
245
+ elif 'application/json' in request.content_type:
246
+ json_data = request.get_json()
247
+ history_data = json_data.get('history', [])
248
+ user_input_part = json_data.get('message')
249
+ else: return jsonify({"msg": "Unsupported format"}), 415
250
+
251
+ history_contents = [msg.get('data') for msg in history_data if msg.get('type') == 'text']
252
+ final_contents = [instruction_prompt] + history_contents + [user_input_part]
253
+ response = client.models.generate_content(model="gemini-2.5-flash", contents=final_contents, config=config)
254
+ response_part = response.candidates[0].content.parts[0]
255
+
256
+ if response_part.function_call:
257
+ function_call = response_part.function_call
258
+
259
+ if function_call.name == "add_items_to_cart":
260
+ proposal_data = {
261
+ "items": function_call.args.get('items', []),
262
+ "delivery_date": function_call.args.get('delivery_date')
263
+ }
264
+ return jsonify({"type": "order_proposal", "data": proposal_data}), 200
265
+
266
+ elif function_call.name == "register_new_customer":
267
+ return jsonify({
268
+ "type": "navigation_proposal",
269
+ "data": {
270
+ "path": "/register",
271
+ "button_text": "Go to Sign Up Page",
272
+ "prompt_text": "Excellent! I can help with that. Please click the button below to go to our registration page."
273
+ }
274
+ }), 200
275
+
276
+ elif function_call.name == "navigate_to_page":
277
+ page = function_call.args.get('page')
278
+ path_map = {'orders': '/order', 'cart': '/cart', 'catalog': '/catalog', 'home': '/', 'about': '/about', 'care': '/care', 'login': '/login', 'register': '/register'}
279
+ text_map = {'orders': 'Go to My Orders', 'cart': 'Go to Cart', 'catalog': 'Go to Catalog', 'home': 'Go to Homepage', 'about': 'Go to About Page', 'care': 'Go to Customer Care', 'login': 'Go to Login', 'register': 'Go to Sign Up'}
280
+ if page in path_map:
281
+ return jsonify({"type": "navigation_proposal", "data": {"path": path_map[page], "button_text": text_map[page], "prompt_text": f"Sure, let's go to the {page} page."}}), 200
282
+
283
+ elif function_call.name == "get_my_orders":
284
+ recent_orders = list(mongo.db.orders.find({'user_email': user_email}).sort('created_at', -1).limit(5))
285
+ if not recent_orders: return jsonify({"type": "text", "data": "You have no recent orders."}), 200
286
+ details_text=""
287
+ for order in recent_orders:
288
+ product_ids = [ObjectId(item['productId']) for item in order.get('items', [])]
289
+ products_map = {str(p['_id']): p for p in mongo.db.products.find({'_id': {'$in': product_ids}})}
290
+ item_lines = [f"- {item['quantity']} {item.get('mode', 'pcs')} of {products_map.get(item['productId'], {}).get('name', 'Unknown Product')}" for item in order.get('items', [])]
291
+ details_text += f"\n\n **Details for Order #{str(order['_id'])[-4:]}** _(Delivery Status: {order.get('status')})_:\n" + "\n".join(item_lines)
292
+ return jsonify({"type": "text", "data": "**Here are your recent orders**:\n" + details_text}), 200
293
+
294
+ elif function_call.name == "cancel_order":
295
+ order_id_frag = function_call.args.get("order_id", "").strip()
296
+ all_user_orders = mongo.db.orders.find({'user_email': user_email})
297
+ order = None
298
+ for o in all_user_orders:
299
+ if str(o['_id']).endswith(order_id_frag):
300
+ order = o
301
+ break
302
+ if not order: return jsonify({"type": "text", "data": f"Sorry, I couldn't find an order with ID matching '{order_id_frag}'."}), 200
303
+ if order.get('status') not in ['pending', 'confirmed']: return jsonify({"type": "text", "data": f"Order #{str(order['_id'])[-4:]} cannot be cancelled as its status is '{order.get('status')}'."}), 200
304
+ return jsonify({
305
+ "type": "cancellation_proposal",
306
+ "data": {"order_id": str(order['_id']), "prompt_text": f"Are you sure you want to cancel order #{str(order['_id'])[-4:]}?"}
307
+ }), 200
308
+
309
+ return jsonify({"type": "text", "data": response.text}), 200
310
+
311
+ @ai_bp.route('/whatsapp', methods=['POST'])
312
+ def whatsapp_reply():
313
+ """ Responds to incoming WhatsApp messages (text or audio) via Twilio. """
314
+ final_response_text = "I'm sorry, I encountered an error. Please try again."
315
+ twilio_resp = MessagingResponse()
316
+
317
+ try:
318
+ # --- 1. Get User and Message from Twilio Request ---
319
+ whatsapp_number = request.values.get('From')
320
+ user_message_text = request.values.get('Body', '').strip()
321
+ num_media = int(request.values.get('NumMedia', 0))
322
+
323
+ if not whatsapp_number:
324
+ current_app.logger.error("Request received without a 'From' number.")
325
+ twilio_resp.message("Could not identify your number. Please try again.")
326
+ return str(twilio_resp)
327
+
328
+ # --- 2. Identify User or Prepare for Registration ---
329
+ user = mongo.db.users.find_one({'whatsapp_number': whatsapp_number})
330
+
331
+ # --- 3. Setup AI Client and Product Context ---
332
+ client = genai.Client(api_key=os.getenv("GEMINI_API_KEY"))
333
+ MODES = ['case', 'bag', 'piece', 'tray', 'weight']
334
+ all_products = list(mongo.db.products.find({}, {'name': 1, 'unit': 1, '_id': 0}))
335
+ product_context_list = []
336
+ for p in all_products:
337
+ if 'name' not in p: continue
338
+ unit_string = p.get('unit', '').lower()
339
+ available_modes = [mode for mode in MODES if mode in unit_string]
340
+ if available_modes:
341
+ product_context_list.append(f"{p['name']} (available units: {', '.join(available_modes)})")
342
+ else:
343
+ product_context_list.append(p['name'])
344
+
345
+ # --- 4. Process User Input (Text or Audio) ---
346
+ user_input_part = None
347
+ if num_media > 0:
348
+ media_url = request.values.get('MediaUrl0')
349
+ mime_type = request.values.get('MediaContentType0')
350
+ if 'audio' in mime_type:
351
+ audio_response = requests.get(media_url)
352
+ if audio_response.status_code == 200:
353
+ user_input_part = types.Part.from_bytes(data=audio_response.content, mime_type=mime_type.split(';')[0])
354
+
355
+ if not user_input_part and user_message_text:
356
+ user_input_part = user_message_text
357
+
358
+ if not user_input_part:
359
+ twilio_resp.message("Please send a message or a voice note.")
360
+ return str(twilio_resp)
361
+
362
+ # --- 5. Configure AI based on User Status and Conversation History ---
363
+ # +++ START: RETRIEVE AND MANAGE CONVERSATION HISTORY +++
364
+ history_doc = mongo.db.whatsapp_history.find_one({'whatsapp_number': whatsapp_number})
365
+ chat_history = history_doc.get('history', []) if history_doc else []
366
+ # +++ END: RETRIEVE AND MANAGE CONVERSATION HISTORY +++
367
+
368
+ if user:
369
+ # User is known, set up for ordering, viewing orders, and placing orders
370
+ tools = types.Tool(function_declarations=[
371
+ add_items_to_cart_function,
372
+ remove_items_from_cart_function, # ADDED
373
+ clear_cart_function, # ADDED
374
+ get_cart_items_function,
375
+ place_order_function,
376
+ get_my_orders_function,
377
+ get_order_details_function,
378
+ ])
379
+ instruction_prompt = f"""
380
+ You are a helpful WhatsApp ordering assistant for A WholeSale Grocery Company Named Matax Express. The user is {user.get('contactPerson', 'a valued customer')}.
381
+ Your goal is to help them manage their order.
382
+ - To add items, use `add_items_to_cart`.
383
+ - To remove specific items from the cart, use `remove_items_from_cart`.
384
+ - To clear the entire cart, use `clear_cart`.
385
+ - To list items in the cart, use `get_cart_items`.
386
+ - To get a summary of recent orders, use `get_my_orders`.
387
+ - To get details for a specific order, use `get_order_details`.
388
+ - To finalize the purchase, use `place_order`. You MUST confirm the delivery date and ask for any special instructions before calling this function.
389
+ Available products: {', '.join(product_context_list)}.
390
+ Always be friendly and confirm actions clearly. Refer to the chat history for context.
391
+ Company Info for general questions: {company_info}.
392
+ **Whenever the user adds items to the cart,places an order , or registers as new customer inform them that they can also view the cart, view their orders and also order on https://matax-express.vercel.app/ logging in with the account they used to register on whatsapp.**
393
+ Finally when the user places the order, inform them that they can view the placed order details on https://matax-express.vercel.app/ by logging in with the account they used to register on whatsapp.
394
+ Always respond in whatsapp formatted markdown manner.
395
+ """
396
+ else:
397
+ # User is new, set up for registration
398
+ tools = types.Tool(function_declarations=[register_new_customer_function])
399
+ instruction_prompt = f"""
400
+ You are a helpful WhatsApp registration assistant forA WholeSale Grocery Company named matax express. The user is new.
401
+ Your primary goal is to register them using the `register_new_customer` function.
402
+ You MUST collect their business name, email, a password, phone number, and business address.
403
+ Ask for the information clearly. Advise the user to choose a secure password.
404
+ Company Info for general questions: {company_info}.
405
+ **Whenever the user adds items to the cart,places an order , or registers as new customer inform them that they can also view the cart, view their orders and also order on https://matax-express.vercel.app/ logging in with the account they used to register on whatsapp.**
406
+ Always respond in whatsapp formatted markdown manner.
407
+ """
408
+
409
+ config = types.GenerateContentConfig(tools=[tools])
410
+
411
+ # +++ START: ADD HISTORY TO THE API CALL +++
412
+ final_contents = [instruction_prompt] + chat_history + [user_input_part]
413
+ response = client.models.generate_content(
414
+ model="gemini-2.5-flash",
415
+ contents=final_contents,
416
+ config=config
417
+ )
418
+ # +++ END: ADD HISTORY TO THE API CALL +++
419
+
420
+ response_part = response.candidates[0].content.parts[0]
421
+
422
+ # --- 6. Process AI Response (Function Call or Text) ---
423
+ if response_part.function_call:
424
+ function_call = response_part.function_call
425
+
426
+ if function_call.name == 'register_new_customer':
427
+ args = function_call.args
428
+ email = args.get('email').lower()
429
+ password = args.get('password')
430
+
431
+ if mongo.db.users.find_one({'email': email}):
432
+ final_response_text = f"An account with the email '{email}' already exists. Please try a different email or contact support."
433
+ elif not password:
434
+ final_response_text = "A password is required to create an account. Please provide a password."
435
+ else:
436
+ hashed_password = bcrypt.generate_password_hash(password).decode('utf-8')
437
+ new_user_doc = {
438
+ "businessName": args.get('businessName'),
439
+ "companyName": args.get('businessName'),
440
+ "email": email,
441
+ "password": hashed_password,
442
+ "phoneNumber": args.get('phoneNumber'),
443
+ "businessAddress": args.get('businessAddress'),
444
+ "contactPerson": args.get('contactPerson'),
445
+ "whatsapp_number": whatsapp_number,
446
+ "is_approved": True,
447
+ "is_admin": False,
448
+ "created_at": datetime.utcnow()
449
+ }
450
+ mongo.db.users.insert_one(new_user_doc)
451
+ final_response_text = f"Welcome, {args.get('businessName')}! You are now registered. You can start placing orders by telling me what you need."
452
+
453
+ elif function_call.name == 'add_items_to_cart' and user:
454
+ items_to_add = function_call.args.get('items', [])
455
+ added_items_messages = []
456
+ db_items = []
457
+
458
+ for item in items_to_add:
459
+ p_name = item.get('product_name')
460
+ product_doc = mongo.db.products.find_one({'name': {'$regex': f'^{p_name}$', '$options': 'i'}})
461
+
462
+ if product_doc:
463
+ db_items.append({
464
+ "productId": str(product_doc['_id']),
465
+ "quantity": item.get('quantity'),
466
+ "mode": item.get('unit')
467
+ })
468
+ added_items_messages.append(f"{item.get('quantity')} {item.get('unit')} of {product_doc['name']}")
469
+ else:
470
+ added_items_messages.append(f"could not find '{p_name}'")
471
+
472
+ if db_items:
473
+ mongo.db.carts.update_one(
474
+ {'user_email': user['email']},
475
+ {'$push': {'items': {'$each': db_items}}, '$set': {'user_email': user['email'], 'updated_at': datetime.utcnow()}},
476
+ upsert=True
477
+ )
478
+ final_response_text = f"OK, I've updated your cart: I added {', '.join(added_items_messages)}. You can view your cart or place the order. What's next?"
479
+
480
+ # +++ START: NEW HANDLER FOR REMOVING ITEMS FROM CART +++
481
+ elif function_call.name == 'remove_items_from_cart' and user:
482
+ items_to_remove = function_call.args.get('items', [])
483
+ removed_product_names = []
484
+ not_found_product_names = []
485
+ product_ids_to_pull = []
486
+
487
+ for item in items_to_remove:
488
+ p_name = item.get('product_name')
489
+ product_doc = mongo.db.products.find_one({'name': {'$regex': f'^{p_name}$', '$options': 'i'}})
490
+ if product_doc:
491
+ product_ids_to_pull.append(str(product_doc['_id']))
492
+ removed_product_names.append(product_doc['name'])
493
+ else:
494
+ not_found_product_names.append(p_name)
495
+
496
+ if product_ids_to_pull:
497
+ mongo.db.carts.update_one(
498
+ {'user_email': user['email']},
499
+ {'$pull': {'items': {'productId': {'$in': product_ids_to_pull}}}}
500
+ )
501
+
502
+ response_parts = []
503
+ if removed_product_names:
504
+ response_parts.append(f"I have removed {', '.join(removed_product_names)} from your cart.")
505
+ if not_found_product_names:
506
+ response_parts.append(f"I couldn't find {', '.join(not_found_product_names)} in your cart to remove.")
507
+
508
+ if not response_parts:
509
+ final_response_text = "I didn't find any items to remove based on your request."
510
+ else:
511
+ final_response_text = " ".join(response_parts)
512
+ # +++ END: NEW HANDLER FOR REMOVING ITEMS FROM CART +++
513
+
514
+ # +++ START: NEW HANDLER FOR CLEARING THE CART +++
515
+ elif function_call.name == 'clear_cart' and user:
516
+ result = mongo.db.carts.update_one(
517
+ {'user_email': user['email']},
518
+ {'$set': {'items': []}}
519
+ )
520
+ if result.matched_count > 0 and result.modified_count > 0:
521
+ final_response_text = "OK, I've cleared all items from your shopping cart."
522
+ else:
523
+ final_response_text = "Your cart is already empty."
524
+ # +++ END: NEW HANDLER FOR CLEARING THE CART +++
525
+
526
+ elif function_call.name == 'get_cart_items' and user:
527
+ cart = mongo.db.carts.find_one({'user_email': user['email']})
528
+ if not cart or not cart.get('items'):
529
+ final_response_text = "Your shopping cart is currently empty."
530
+ else:
531
+ cart_items = cart.get('items', [])
532
+ product_ids = [ObjectId(item['productId']) for item in cart_items if 'productId' in item]
533
+ products_map = {str(p['_id']): p for p in mongo.db.products.find({'_id': {'$in': product_ids}})}
534
+
535
+ item_lines = []
536
+ for item in cart_items:
537
+ product_details = products_map.get(item.get('productId'), {})
538
+ product_name = product_details.get('name', 'Unknown Product')
539
+ item_lines.append(f"- {item.get('quantity')} {item.get('mode', 'pcs')} of {product_name}")
540
+
541
+ if item_lines:
542
+ final_response_text = "*Here are the items in your cart:*\n" + "\n".join(item_lines)
543
+ else:
544
+ final_response_text = "Your shopping cart is currently empty."
545
+
546
+ elif function_call.name == 'place_order' and user:
547
+ cart = mongo.db.carts.find_one({'user_email': user['email']})
548
+ if not cart or not cart.get('items'):
549
+ final_response_text = "Your cart is empty. Please add some items before placing an order."
550
+ else:
551
+ args = function_call.args
552
+ order_doc = {
553
+ 'user_email': user['email'],
554
+ 'items': cart['items'],
555
+ 'delivery_date': args.get('delivery_date'),
556
+ 'delivery_address': user.get('businessAddress'),
557
+ 'mobile_number': user.get('phoneNumber'),
558
+ 'additional_info': args.get('additional_info', ''),
559
+ 'status': 'pending',
560
+ 'created_at': datetime.utcnow()
561
+ }
562
+ order_id = mongo.db.orders.insert_one(order_doc).inserted_id
563
+ mongo.db.carts.delete_one({'user_email': user['email']}) # Clear cart
564
+ final_response_text = f"Thank you! Your order #{str(order_id)[-6:]} has been placed for delivery on {args.get('delivery_date')}. You will receive a confirmation shortly."
565
+
566
+ elif function_call.name == 'get_my_orders' and user:
567
+ recent_orders = list(mongo.db.orders.find({'user_email': user['email']}).sort('created_at', -1).limit(3))
568
+ if not recent_orders:
569
+ final_response_text = "You don't have any recent orders."
570
+ else:
571
+ details_text = "Here are your last few orders:\n"
572
+ for order in recent_orders:
573
+ details_text += f"\n- Order #{str(order['_id'])[-6:]} (Status: {order.get('status')}) on {order.get('delivery_date')}"
574
+ final_response_text = details_text
575
+
576
+ elif function_call.name == 'get_order_details' and user:
577
+ order_id_frag = function_call.args.get("order_id", "").strip()
578
+ all_user_orders = mongo.db.orders.find({'user_email': user['email']})
579
+ order_doc = None
580
+ for o in all_user_orders:
581
+ if str(o['_id']).endswith(order_id_frag):
582
+ order_doc = o
583
+ break
584
+
585
+ if not order_doc:
586
+ final_response_text = f"Sorry, I couldn't find an order with ID matching '{order_id_frag}'."
587
+ else:
588
+ product_ids = [ObjectId(item['productId']) for item in order_doc.get('items', [])]
589
+ products_map = {str(p['_id']): p for p in mongo.db.products.find({'_id': {'$in': product_ids}})}
590
+ item_lines = [f"- {item['quantity']} {item.get('mode', 'pcs')} of {products_map.get(item['productId'], {}).get('name', 'Unknown Product')}" for item in order_doc.get('items', [])]
591
+ details_text = f"*Details for Order #{str(order_doc['_id'])[-6:]}*\n*Status:* {order_doc.get('status', 'N/A')}\n*Delivery Date:* {order_doc.get('delivery_date', 'N/A')}\n\n*Items:*\n" + "\n".join(item_lines)
592
+ final_response_text = details_text
593
+ else:
594
+ final_response_text = response.text
595
+
596
+ # +++ START: SAVE CONVERSATION TO DATABASE +++
597
+ user_message_to_save = user_message_text if user_message_text else "Audio message"
598
+ chat_history.append({"role": "user", "parts": [{"text": user_message_to_save}]})
599
+ chat_history.append({"role": "model", "parts": [{"text": final_response_text}]})
600
+
601
+ # Keep the history from getting too long
602
+ if len(chat_history) > 10:
603
+ chat_history = chat_history[-10:]
604
+
605
+ mongo.db.whatsapp_history.update_one(
606
+ {'whatsapp_number': whatsapp_number},
607
+ {'$set': {'history': chat_history, 'updated_at': datetime.utcnow()}},
608
+ upsert=True
609
+ )
610
+ # +++ END: SAVE CONVERSATION TO DATABASE +++
611
+
612
+ except Exception as e:
613
+ current_app.logger.error(f"WhatsApp endpoint error: {e}")
614
+ final_response_text = "I'm having a little trouble right now. Please try again in a moment."
615
+
616
+ twilio_resp.message(final_response_text)
617
+ return str(twilio_resp)
app/api.py CHANGED
@@ -1,234 +1,22 @@
1
  # your_app/api.py
2
 
3
- from flask import Blueprint, request, jsonify, current_app,redirect
4
  from bson.objectid import ObjectId
5
- from datetime import datetime,date
6
  from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity
7
  from .extensions import bcrypt, mongo
8
  from .xero_utils import trigger_po_creation, trigger_contact_creation
9
  from .email_utils import send_order_confirmation_email, send_registration_email, send_login_notification_email, send_cart_reminder_email
10
- from google import genai
11
- from google.genai import types
12
- from datetime import datetime, timedelta # <-- Import timedelta
13
- import os
14
- import json
15
- from .Company_Info import company_info
16
- api_bp = Blueprint('api', __name__)
17
 
18
 
19
- today = date.today()
20
- weekday_number = today.weekday()
21
- days_of_week = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
22
- weekday_name = days_of_week[weekday_number]
23
-
24
- # --- NEW: Define functions for the AI model ---
25
-
26
- add_items_to_cart_function = {
27
- "name": "add_items_to_cart",
28
- "description": "Extracts order details from a user's message to add products to their shopping cart. Can also set a delivery date.",
29
- "parameters": {
30
- "type": "object", "properties": {
31
- "items": { "type": "array", "description": "A list of items to add to the cart.", "items": { "type": "object", "properties": { "product_name": { "type": "string", "description": "The name of the product. This must match one of the available product names.", }, "quantity": { "type": "number", "description": "The quantity of the product.", }, "unit": { "type": "string", "description": "The unit of measure for the quantity (e.g., 'lb', 'pieces', 'box', 'bunch'). This must match one of the available units for the product.", }, }, "required": ["product_name", "quantity", "unit"], }, },
32
- # --- START NEW PARAMETER ---
33
- "delivery_date": {
34
- "type": "string",
35
- "description": "The desired delivery date for the order, in YYYY-MM-DD format. The user must explicitly state a date.",
36
- }
37
- # --- END NEW PARAMETER ---
38
- },
39
- "required": ["items"],
40
- },
41
- }
42
-
43
- navigate_to_page_function = {
44
- "name": "navigate_to_page",
45
- "description": "Provides a button in the chat to navigate the user to a specific page.",
46
- "parameters": {
47
- "type": "object",
48
- "properties": {
49
- "page": {
50
- "type": "string",
51
- "description": "The destination page. Must be one of the allowed pages.",
52
- "enum": ["orders", "cart", "catalog", "home", "about", "care", "login", "register"]
53
- }
54
- },
55
- "required": ["page"]
56
- }
57
- }
58
-
59
- get_my_orders_function = {
60
- "name": "get_my_orders",
61
- "description": "Retrieves a summary of the user's most recent orders to display in the chat.",
62
- "parameters": {"type": "object", "properties": {}}
63
- }
64
-
65
- # --- NEW: Function to get details of a specific order ---
66
- get_order_details_function = {
67
- "name": "get_order_details",
68
- "description": "Retrieves the specific items and details for a single order based on its ID.",
69
- "parameters": {
70
- "type": "object",
71
- "properties": {
72
- "order_id": {
73
- "type": "string",
74
- "description": "The ID of the order to fetch. Can be the full ID or the last 6 characters."
75
- }
76
- },
77
- "required": ["order_id"]
78
- }
79
- }
80
-
81
- # --- NEW: Function to initiate an order cancellation ---
82
- cancel_order_function = {
83
- "name": "cancel_order",
84
- "description": "Proposes to cancel an order based on its ID. This requires user confirmation.",
85
- "parameters": {
86
- "type": "object",
87
- "properties": {
88
- "order_id": {
89
- "type": "string",
90
- "description": "The ID of the order to be cancelled. Can be the full ID or the last 6 characters."
91
- }
92
- },
93
- "required": ["order_id"]
94
- }
95
- }
96
 
 
97
 
98
  @api_bp.route('/clear')
99
  def clear_all():
100
- # mongo.db.users.delete_many({})
101
  mongo.db.orders.delete_many({})
102
  return "✅"
103
 
104
-
105
- @api_bp.route('/chat', methods=['POST'])
106
- @jwt_required()
107
- def handle_ai_chat():
108
- # try:
109
- # --- 1. Universal Setup ---
110
- user_email = get_jwt_identity()
111
- MODES = ['case', 'bag', 'piece', 'tray', 'weight']
112
- all_products = list(mongo.db.products.find({}, {'name': 1, 'unit': 1, '_id': 0}))
113
- product_context_list = []
114
- for p in all_products:
115
- if 'name' not in p: continue
116
- unit_string = p.get('unit', '').lower()
117
- available_modes = [mode for mode in MODES if mode in unit_string]
118
- if available_modes:
119
- product_context_list.append(f"{p['name']} (available units: {', '.join(available_modes)})")
120
- else:
121
- product_context_list.append(p['name'])
122
- client = genai.Client(api_key="AIzaSyB7yKIdfW7Umv62G47BCdJjoHTJ9TeiAko")
123
-
124
- # --- START MODIFICATION: Add all new functions to the tools list and calculate dates ---
125
- current_date = datetime.now().strftime('%Y-%m-%d')
126
- min_delivery_date = (datetime.now() + timedelta(days=2)).strftime('%Y-%m-%d')
127
-
128
- tools = types.Tool(function_declarations=[
129
- add_items_to_cart_function,
130
- navigate_to_page_function,
131
- get_my_orders_function,
132
- cancel_order_function
133
- ])
134
- config = types.GenerateContentConfig(tools=[tools])
135
-
136
- # --- 2. UPDATED: Prepare the main instruction prompt for the AI ---
137
- instruction_prompt = f"""
138
- You are an expert assistant for a food wholesale company. Your capabilities include:
139
- 1. Ordering & Delivery Date: Use `add_items_to_cart` to add items to the user's cart. You can also set a delivery date if the user specifies one. The current date is {current_date,weekday_name}. The earliest possible delivery date is {min_delivery_date}. If the user asks for a date earlier than this, inform them of the constraint and ask them to provide a new valid date.
140
- 2. Product List: {'; '.join(product_context_list)}.
141
- 3. Page Navigation: Use `navigate_to_page` if the user wants to GO to a page like 'my orders', 'about us', 'customer care', 'login', etc.
142
- 4. Order Summary: Use `get_my_orders` to LIST a summary of recent orders in the chat.
143
- 5. Order Cancellation: Use `cancel_order` to start the cancellation process for a SPECIFIC order. The user must provide an order ID.
144
- 6. The Capability You must use AT ALL TIMES: Luring the customer to place an order and persuading them to buy more products.
145
- *Always Brag about the quality of our products and our customer service and try to persuade the customer to place an order.*
146
-
147
-
148
- Company Info for general questions: {company_info}
149
- *** IMPORTANT: When providing a text-based answer, YOU MUST USE MARKDOWN. Use headings, bold text, bullet points (`* item`), and other formatting to make your answers clear, structured, and easy to read. ***
150
- Always refer to the chat history for context. **If information is missing, ask for it.**
151
- """
152
- # --- END MODIFICATION ---
153
-
154
- # --- 3. Process incoming user input (This section remains the same) ---
155
- user_input_part, history_data = None, []
156
- if 'multipart/form-data' in request.content_type:
157
- audio_file = request.files.get('audio')
158
- history_json = request.form.get('history', '[]')
159
- history_data = json.loads(history_json)
160
- user_input_part = types.Part.from_bytes(data=audio_file.read(), mime_type=audio_file.mimetype.split(';')[0])
161
- elif 'application/json' in request.content_type:
162
- json_data = request.get_json()
163
- history_data = json_data.get('history', [])
164
- user_input_part = json_data.get('message')
165
- else: return jsonify({"msg": "Unsupported format"}), 415
166
-
167
- # --- 4. Make API call (This section remains the same) ---
168
- history_contents = [msg.get('data') for msg in history_data if msg.get('type') == 'text']
169
- final_contents = [instruction_prompt] + history_contents + [user_input_part]
170
- response = client.models.generate_content(model="gemini-2.5-flash", contents=final_contents, config=config)
171
- response_part = response.candidates[0].content.parts[0]
172
-
173
- # --- 5. UPDATED: Process the response with new function handlers ---
174
- if response_part.function_call:
175
- function_call = response_part.function_call
176
-
177
- if function_call.name == "add_items_to_cart":
178
- # --- START MODIFICATION ---
179
- # Create a payload with both items and the potential delivery date
180
- proposal_data = {
181
- "items": function_call.args.get('items', []),
182
- "delivery_date": function_call.args.get('delivery_date') # This will be None if not provided by the AI
183
- }
184
- return jsonify({"type": "order_proposal", "data": proposal_data}), 200
185
- # --- END MODIFICATION ---
186
-
187
- elif function_call.name == "navigate_to_page":
188
- page = function_call.args.get('page')
189
- path_map = {'orders': '/order', 'cart': '/cart', 'catalog': '/catalog', 'home': '/', 'about': '/about', 'care': '/care', 'login': '/login', 'register': '/register'}
190
- text_map = {'orders': 'Go to My Orders', 'cart': 'Go to Cart', 'catalog': 'Go to Catalog', 'home': 'Go to Homepage', 'about': 'Go to About Page', 'care': 'Go to Customer Care', 'login': 'Go to Login', 'register': 'Go to Sign Up'}
191
- if page in path_map:
192
- return jsonify({"type": "navigation_proposal", "data": {"path": path_map[page], "button_text": text_map[page], "prompt_text": f"Sure, let's go to the {page} page."}}), 200
193
-
194
- elif function_call.name == "get_my_orders":
195
- recent_orders = list(mongo.db.orders.find({'user_email': user_email}).sort('created_at', -1).limit(5))
196
- if not recent_orders: return jsonify({"type": "text", "data": "You have no recent orders."}), 200
197
- summary = "\n".join([f"- Order Details for #{str(o['_id'])[-6:]}\n{str(o)} \n\n" for o in recent_orders])
198
- details_text=""
199
- for order in recent_orders:
200
- product_ids = [ObjectId(item['productId']) for item in order.get('items', [])]
201
- products_map = {str(p['_id']): p for p in mongo.db.products.find({'_id': {'$in': product_ids}})}
202
- item_lines = [f"- {item['quantity']} {item.get('mode', 'pcs')} of {products_map.get(item['productId'], {}).get('name', 'Unknown Product')}" for item in order.get('items', [])]
203
- details_text += f"\n\n **Details for Order #{str(order['_id'])[-4:]}** _(Delivery Status: {order.get('status')})_:\n" + "\n".join(item_lines)
204
- return jsonify({"type": "text", "data": "**Here are your recent orders**:\n" + details_text}), 200
205
-
206
- elif function_call.name == "cancel_order":
207
- order_id_frag = function_call.args.get("order_id", "").strip()
208
-
209
- all_user_orders = mongo.db.orders.find({'user_email': user_email})
210
- order = None
211
- for o in all_user_orders:
212
- if str(o['_id']).endswith(order_id_frag):
213
- order = o
214
- break
215
- if not order: return jsonify({"type": "text", "data": f"Sorry, I couldn't find an order with ID matching '{order_id_frag}'."}), 200
216
- if order.get('status') not in ['pending', 'confirmed']: return jsonify({"type": "text", "data": f"Order #{str(order['_id'])[-4:]} cannot be cancelled as its status is '{order.get('status')}'."}), 200
217
-
218
- return jsonify({
219
- "type": "cancellation_proposal",
220
- "data": {"order_id": str(order['_id']), "prompt_text": f"Are you sure you want to cancel order #{str(order['_id'])[-4:]}?"}
221
- }), 200
222
-
223
- # Fallback to text response
224
- return jsonify({"type": "text", "data": response.text}), 200
225
-
226
- # except Exception as e:
227
- # current_app.logger.error(f"Gemini API Error: {e}")
228
- # return jsonify({"msg": "Sorry, I'm having trouble. Please try again."}), 500
229
-
230
- # --- The rest of your api.py file remains unchanged ---
231
-
232
  @api_bp.route('/register', methods=['POST'])
233
  def register():
234
  data = request.get_json()
@@ -246,7 +34,7 @@ def register():
246
  user_document = data.copy()
247
  user_document['password'] = hashed_password
248
  user_document['company_name'] = company_name
249
- user_document['is_approved'] = False
250
  user_document['is_admin'] = False
251
 
252
  mongo.db.users.insert_one(user_document)
@@ -259,13 +47,14 @@ def register():
259
 
260
  return jsonify({"msg": "Registration successful! Your application is being processed."}), 201
261
 
 
262
  @api_bp.route('/login', methods=['POST'])
263
  def login():
264
  data = request.get_json()
265
  email, password = data.get('email'), data.get('password')
266
  user = mongo.db.users.find_one({'email': email})
267
 
268
- if user and bcrypt.check_password_hash(user['password'], password):
269
  if not user.get('is_approved', False): return jsonify({"msg": "Account pending approval"}), 403
270
 
271
  try:
@@ -274,7 +63,7 @@ def login():
274
  current_app.logger.error(f"Failed to send login notification email to {email}: {e}")
275
 
276
  access_token = create_access_token(identity=email)
277
- return jsonify(access_token=access_token, email=user['email'], companyName=user['company_name'],contactPerson=user.get('contactPerson', '')) , 200
278
 
279
  return jsonify({"msg": "Bad email or password"}), 401
280
 
@@ -333,7 +122,6 @@ def handle_cart():
333
  })
334
 
335
  if request.method == 'POST':
336
- # --- START MODIFICATION: Handle partial updates to prevent data loss ---
337
  data = request.get_json()
338
 
339
  update_doc = {
@@ -341,7 +129,6 @@ def handle_cart():
341
  'updated_at': datetime.utcnow()
342
  }
343
 
344
- # Only update fields that are present in the request
345
  if 'items' in data:
346
  update_doc['items'] = data['items']
347
 
@@ -353,7 +140,6 @@ def handle_cart():
353
  {'$set': update_doc},
354
  upsert=True
355
  )
356
- # --- END MODIFICATION ---
357
  return jsonify({"msg": "Cart updated successfully"})
358
 
359
  @api_bp.route('/orders', methods=['GET', 'POST'])
@@ -496,21 +282,14 @@ def cancel_order(order_id):
496
 
497
  return jsonify({"msg": "Order has been cancelled."}), 200
498
 
499
- # +++ START: NEW /sendmail ENDPOINT +++
500
  @api_bp.route('/sendmail', methods=['GET'])
501
  def send_cart_reminders():
502
- """
503
- Triggers reminder emails to all users with items in their cart.
504
- This can be triggered manually or by a scheduled job (e.g., cron).
505
- """
506
  try:
507
- # 1. Find all carts that have items
508
  carts_with_items = list(mongo.db.carts.find({'items': {'$exists': True, '$ne': []}}))
509
 
510
  if not carts_with_items:
511
  return jsonify({"msg": "No users with pending items in cart."}), 200
512
 
513
- # 2. Collect all user emails and product IDs for efficient bulk fetching
514
  user_emails = [cart['user_email'] for cart in carts_with_items]
515
  all_product_ids = {
516
  ObjectId(item['productId'])
@@ -518,24 +297,20 @@ def send_cart_reminders():
518
  for item in cart.get('items', [])
519
  }
520
 
521
- # 3. Fetch all required users and products from the DB
522
  users_cursor = mongo.db.users.find({'email': {'$in': user_emails}})
523
  products_cursor = mongo.db.products.find({'_id': {'$in': list(all_product_ids)}})
524
 
525
- # 4. Create lookup maps for fast access
526
  users_map = {user['email']: user for user in users_cursor}
527
  products_map = {str(prod['_id']): prod for prod in products_cursor}
528
 
529
  emails_sent_count = 0
530
 
531
- # 5. Iterate through carts, prepare data, and send emails
532
  for cart in carts_with_items:
533
  user = users_map.get(cart['user_email'])
534
  if not user:
535
  current_app.logger.warning(f"Cart found for non-existent user: {cart['user_email']}")
536
  continue
537
 
538
- # Populate cart items with full product details
539
  populated_items = []
540
  for item in cart.get('items', []):
541
  product_details = products_map.get(item['productId'])
@@ -560,7 +335,6 @@ def send_cart_reminders():
560
  except Exception as e:
561
  current_app.logger.error(f"Error in /sendmail endpoint: {e}")
562
  return jsonify({"msg": "An internal error occurred while sending reminders."}), 500
563
- # +++ END: NEW /sendmail ENDPOINT +++
564
 
565
  @api_bp.route('/admin/users/approve/<user_id>', methods=['POST'])
566
  @jwt_required()
@@ -568,168 +342,6 @@ def approve_user(user_id):
568
  mongo.db.users.update_one({'_id': ObjectId(user_id)}, {'$set': {'is_approved': True}})
569
  return jsonify({"msg": f"User {user_id} approved"})
570
 
571
-
572
- # Default data for initialization if pages don't exist in the database
573
- DEFAULT_ABOUT_DATA = {
574
- "_id": "about",
575
- "title": "Our Commitment to You",
576
- "slogan": "It's more than produce — it's about being your daily, trusted partner.",
577
- "paragraphs": [
578
- "Welcome to Matax Express Ltd, your trusted wholesale produce provider serving the Greater Toronto Area for over 30 years!",
579
- "At Matax Express Ltd, our commitment to service has always been at the core of what we do. From day one, we’ve focused on understanding the needs of our customers and working tirelessly to meet them. Whether delivering to bustling restaurants, local markets, or retail stores, we strive to ensure your success and satisfaction every step of the way.",
580
- "We’re proud of the journey we’ve taken over the past three decades, and we also understand the importance of continuous improvement. Your feedback has been crucial in helping us grow and adapt, and we are working hard to ensure every interaction reflects the high standard of service you deserve.",
581
- "While freshness remains an important priority, we are equally dedicated to creating a service experience that exceeds expectations. From reliable deliveries to personalized support, our team is here to make partnering with Matax Express Ltd seamless and efficient.",
582
- "For us, it’s not just about providing produce—it’s about being a dependable partner that you can count on daily. We look forward to building stronger relationships and delivering better service for years to come."
583
- ]
584
- }
585
-
586
- DEFAULT_CONTACT_DATA = {
587
- "_id": "contact",
588
- "title": "Contact Us",
589
- "intro": "We're here to help! Reach out to us through any of the channels below. We aim to respond to all inquiries within 24 business hours.",
590
- "details": [
591
- {"type": "Phone Support", "value": "+1 (800) 123-4567"},
592
- {"type": "Email Support", "value": "support@mataxexpress.com"},
593
- {"type": "Business Hours", "value": "Monday - Friday, 9:00 AM - 5:00 PM (EST)"},
594
- {"type": "Mailing Address", "value": "123 Fresh Produce Lane, Farmville, ST 54321"}
595
- ]
596
- }
597
-
598
- @api_bp.route('/pages/<page_name>', methods=['GET'])
599
- def get_page_content(page_name):
600
- """API endpoint for the frontend to fetch page content."""
601
- content = mongo.db.pages.find_one({'_id': page_name})
602
- if not content:
603
- # If content doesn't exist, create it from default and return it
604
- if page_name == 'about':
605
- mongo.db.pages.insert_one(DEFAULT_ABOUT_DATA)
606
- content = DEFAULT_ABOUT_DATA
607
- elif page_name == 'contact':
608
- mongo.db.pages.insert_one(DEFAULT_CONTACT_DATA)
609
- content = DEFAULT_CONTACT_DATA
610
- else:
611
- return jsonify({"msg": "Page not found"}), 404
612
- # Ensure _id is a string if it's an ObjectId
613
- if '_id' in content and not isinstance(content['_id'], str):
614
- content['_id'] = str(content['_id'])
615
- return jsonify(content)
616
-
617
- @api_bp.route('/pages/update', methods=['POST'])
618
- def update_page_content():
619
- """Handles form submission from the /update UI to save changes."""
620
- page_name = request.form.get('page_name')
621
- if page_name == 'about':
622
- paragraphs = request.form.get('paragraphs', '').strip().split('\n')
623
- paragraphs = [p.strip() for p in paragraphs if p.strip()]
624
-
625
- update_data = {
626
- "title": request.form.get('title'),
627
- "slogan": request.form.get('slogan'),
628
- "paragraphs": paragraphs
629
- }
630
- elif page_name == 'contact':
631
- update_data = {
632
- "title": request.form.get('title'),
633
- "intro": request.form.get('intro'),
634
- "details": [
635
- {"type": "Phone Support", "value": request.form.get('phone_value')},
636
- {"type": "Email Support", "value": request.form.get('email_value')},
637
- {"type": "Business Hours", "value": request.form.get('hours_value')},
638
- {"type": "Mailing Address", "value": request.form.get('address_value')}
639
- ]
640
- }
641
- else:
642
- return redirect('/api/update')
643
-
644
- mongo.db.pages.update_one(
645
- {'_id': page_name},
646
- {'$set': update_data},
647
- upsert=True
648
- )
649
- return redirect('/api/update')
650
-
651
- @api_bp.route('/update', methods=['GET'])
652
- def update_ui():
653
- """Serves the simple HTML UI for editing page content."""
654
- about_data = mongo.db.pages.find_one({'_id': 'about'}) or DEFAULT_ABOUT_DATA
655
- contact_data = mongo.db.pages.find_one({'_id': 'contact'}) or DEFAULT_CONTACT_DATA
656
-
657
- about_paragraphs_text = "\n".join(about_data.get('paragraphs', []))
658
- contact_details = {item['type']: item['value'] for item in contact_data.get('details', [])}
659
-
660
- html = f"""
661
- <!DOCTYPE html>
662
- <html lang="en">
663
- <head>
664
- <meta charset="UTF-8">
665
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
666
- <title>Update Page Content</title>
667
- <style>
668
- body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 2em; background-color: #f8f9fa; color: #212529; }}
669
- .container {{ max-width: 800px; margin: auto; }}
670
- h1, h2 {{ color: #343a40; border-bottom: 2px solid #dee2e6; padding-bottom: 0.5em; }}
671
- form {{ background: white; padding: 2em; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.05); margin-bottom: 2em; }}
672
- label {{ display: block; margin-top: 1em; margin-bottom: 0.5em; font-weight: bold; color: #495057; }}
673
- input[type="text"], textarea {{ width: 100%; padding: 0.8em; border: 1px solid #ced4da; border-radius: 4px; box-sizing: border-box; font-size: 1rem; }}
674
- textarea {{ height: 250px; resize: vertical; }}
675
- button {{ background-color: #007bff; color: white; padding: 0.8em 1.5em; border: none; border-radius: 4px; cursor: pointer; font-size: 1em; font-weight: bold; }}
676
- button:hover {{ background-color: #0056b3; }}
677
- </style>
678
- </head>
679
- <body>
680
- <div class="container">
681
- <h1>Update Website Content</h1>
682
-
683
- <!-- About Us Page Form -->
684
- <h2>About Us Page</h2>
685
- <form action="/api/pages/update" method="post">
686
- <input type="hidden" name="page_name" value="about">
687
- <label for="about_title">Title:</label>
688
- <input type="text" id="about_title" name="title" value="{about_data.get('title', '')}">
689
-
690
- <label for="about_slogan">Slogan:</label>
691
- <input type="text" id="about_slogan" name="slogan" value="{about_data.get('slogan', '')}">
692
-
693
- <label for="about_paragraphs">Paragraphs (one paragraph per line):</label>
694
- <textarea id="about_paragraphs" name="paragraphs">{about_paragraphs_text}</textarea>
695
-
696
- <br><br>
697
- <button type="submit">Update About Page</button>
698
- </form>
699
-
700
- <!-- Contact Page Form -->
701
- <h2>Customer Care Page</h2>
702
- <form action="/api/pages/update" method="post">
703
- <input type="hidden" name="page_name" value="contact">
704
-
705
- <label for="contact_title">Title:</label>
706
- <input type="text" id="contact_title" name="title" value="{contact_data.get('title', '')}">
707
-
708
- <label for="contact_intro">Intro Text:</label>
709
- <input type="text" id="contact_intro" name="intro" value="{contact_data.get('intro', '')}">
710
-
711
- <label for="phone_value">Phone Support:</label>
712
- <input type="text" id="phone_value" name="phone_value" value="{contact_details.get('Phone Support', '')}">
713
-
714
- <label for="email_value">Email Support:</label>
715
- <input type="text" id="email_value" name="email_value" value="{contact_details.get('Email Support', '')}">
716
-
717
- <label for="hours_value">Business Hours:</label>
718
- <input type="text" id="hours_value" name="hours_value" value="{contact_details.get('Business Hours', '')}">
719
-
720
- <label for="address_value">Mailing Address:</label>
721
- <input type="text" id="address_value" name="address_value" value="{contact_details.get('Mailing Address', '')}">
722
-
723
- <br><br>
724
- <button type="submit">Update Contact Page</button>
725
- </form>
726
- </div>
727
- </body>
728
- </html>
729
- """
730
- return html
731
-
732
-
733
  # +++ START: NEW ENDPOINT FOR ITEM REQUESTS +++
734
  @api_bp.route('/request-item', methods=['POST'])
735
  @jwt_required()
 
1
  # your_app/api.py
2
 
3
+ from flask import Blueprint, request, jsonify, current_app, redirect
4
  from bson.objectid import ObjectId
5
+ from datetime import datetime
6
  from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity
7
  from .extensions import bcrypt, mongo
8
  from .xero_utils import trigger_po_creation, trigger_contact_creation
9
  from .email_utils import send_order_confirmation_email, send_registration_email, send_login_notification_email, send_cart_reminder_email
 
 
 
 
 
 
 
10
 
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
+ api_bp = Blueprint('api', __name__)
14
 
15
  @api_bp.route('/clear')
16
  def clear_all():
 
17
  mongo.db.orders.delete_many({})
18
  return "✅"
19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  @api_bp.route('/register', methods=['POST'])
21
  def register():
22
  data = request.get_json()
 
34
  user_document = data.copy()
35
  user_document['password'] = hashed_password
36
  user_document['company_name'] = company_name
37
+ user_document['is_approved'] = True
38
  user_document['is_admin'] = False
39
 
40
  mongo.db.users.insert_one(user_document)
 
47
 
48
  return jsonify({"msg": "Registration successful! Your application is being processed."}), 201
49
 
50
+ # ... (the rest of your api.py file remains unchanged)
51
  @api_bp.route('/login', methods=['POST'])
52
  def login():
53
  data = request.get_json()
54
  email, password = data.get('email'), data.get('password')
55
  user = mongo.db.users.find_one({'email': email})
56
 
57
+ if user and user.get('password') and bcrypt.check_password_hash(user['password'], password):
58
  if not user.get('is_approved', False): return jsonify({"msg": "Account pending approval"}), 403
59
 
60
  try:
 
63
  current_app.logger.error(f"Failed to send login notification email to {email}: {e}")
64
 
65
  access_token = create_access_token(identity=email)
66
+ return jsonify(access_token=access_token, email=user['email'], companyName=user['businessName'],contactPerson=user.get('contactPerson', '')) , 200
67
 
68
  return jsonify({"msg": "Bad email or password"}), 401
69
 
 
122
  })
123
 
124
  if request.method == 'POST':
 
125
  data = request.get_json()
126
 
127
  update_doc = {
 
129
  'updated_at': datetime.utcnow()
130
  }
131
 
 
132
  if 'items' in data:
133
  update_doc['items'] = data['items']
134
 
 
140
  {'$set': update_doc},
141
  upsert=True
142
  )
 
143
  return jsonify({"msg": "Cart updated successfully"})
144
 
145
  @api_bp.route('/orders', methods=['GET', 'POST'])
 
282
 
283
  return jsonify({"msg": "Order has been cancelled."}), 200
284
 
 
285
  @api_bp.route('/sendmail', methods=['GET'])
286
  def send_cart_reminders():
 
 
 
 
287
  try:
 
288
  carts_with_items = list(mongo.db.carts.find({'items': {'$exists': True, '$ne': []}}))
289
 
290
  if not carts_with_items:
291
  return jsonify({"msg": "No users with pending items in cart."}), 200
292
 
 
293
  user_emails = [cart['user_email'] for cart in carts_with_items]
294
  all_product_ids = {
295
  ObjectId(item['productId'])
 
297
  for item in cart.get('items', [])
298
  }
299
 
 
300
  users_cursor = mongo.db.users.find({'email': {'$in': user_emails}})
301
  products_cursor = mongo.db.products.find({'_id': {'$in': list(all_product_ids)}})
302
 
 
303
  users_map = {user['email']: user for user in users_cursor}
304
  products_map = {str(prod['_id']): prod for prod in products_cursor}
305
 
306
  emails_sent_count = 0
307
 
 
308
  for cart in carts_with_items:
309
  user = users_map.get(cart['user_email'])
310
  if not user:
311
  current_app.logger.warning(f"Cart found for non-existent user: {cart['user_email']}")
312
  continue
313
 
 
314
  populated_items = []
315
  for item in cart.get('items', []):
316
  product_details = products_map.get(item['productId'])
 
335
  except Exception as e:
336
  current_app.logger.error(f"Error in /sendmail endpoint: {e}")
337
  return jsonify({"msg": "An internal error occurred while sending reminders."}), 500
 
338
 
339
  @api_bp.route('/admin/users/approve/<user_id>', methods=['POST'])
340
  @jwt_required()
 
342
  mongo.db.users.update_one({'_id': ObjectId(user_id)}, {'$set': {'is_approved': True}})
343
  return jsonify({"msg": f"User {user_id} approved"})
344
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
  # +++ START: NEW ENDPOINT FOR ITEM REQUESTS +++
346
  @api_bp.route('/request-item', methods=['POST'])
347
  @jwt_required()
app/page_features.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # your_app/page_features.py
2
+
3
+ from flask import Blueprint, request, jsonify, redirect
4
+ from .extensions import mongo
5
+
6
+ page_bp = Blueprint('pages', __name__)
7
+
8
+ # Default data for initialization if pages don't exist in the database
9
+ DEFAULT_ABOUT_DATA = {
10
+ "_id": "about",
11
+ "title": "Our Commitment to You",
12
+ "slogan": "It's more than produce — it's about being your daily, trusted partner.",
13
+ "paragraphs": [
14
+ "Welcome to Matax Express Ltd, your trusted wholesale produce provider serving the Greater Toronto Area for over 30 years!",
15
+ "At Matax Express Ltd, our commitment to service has always been at the core of what we do. From day one, we’ve focused on understanding the needs of our customers and working tirelessly to meet them. Whether delivering to bustling restaurants, local markets, or retail stores, we strive to ensure your success and satisfaction every step of the way.",
16
+ "We’re proud of the journey we’ve taken over the past three decades, and we also understand the importance of continuous improvement. Your feedback has been crucial in helping us grow and adapt, and we are working hard to ensure every interaction reflects the high standard of service you deserve.",
17
+ "While freshness remains an important priority, we are equally dedicated to creating a service experience that exceeds expectations. From reliable deliveries to personalized support, our team is here to make partnering with Matax Express Ltd seamless and efficient.",
18
+ "For us, it’s not just about providing produce—it’s about being a dependable partner that you can count on daily. We look forward to building stronger relationships and delivering better service for years to come."
19
+ ]
20
+ }
21
+
22
+ DEFAULT_CONTACT_DATA = {
23
+ "_id": "contact",
24
+ "title": "Contact Us",
25
+ "intro": "We're here to help! Reach out to us through any of the channels below. We aim to respond to all inquiries within 24 business hours.",
26
+ "details": [
27
+ {"type": "Phone Support", "value": "+1 (800) 123-4567"},
28
+ {"type": "Email Support", "value": "support@mataxexpress.com"},
29
+ {"type": "Business Hours", "value": "Monday - Friday, 9:00 AM - 5:00 PM (EST)"},
30
+ {"type": "Mailing Address", "value": "123 Fresh Produce Lane, Farmville, ST 54321"}
31
+ ]
32
+ }
33
+
34
+ @page_bp.route('/<page_name>', methods=['GET'])
35
+ def get_page_content(page_name):
36
+ """API endpoint for the frontend to fetch page content."""
37
+ content = mongo.db.pages.find_one({'_id': page_name})
38
+ if not content:
39
+ # If content doesn't exist, create it from default and return it
40
+ if page_name == 'about':
41
+ mongo.db.pages.insert_one(DEFAULT_ABOUT_DATA)
42
+ content = DEFAULT_ABOUT_DATA
43
+ elif page_name == 'contact':
44
+ mongo.db.pages.insert_one(DEFAULT_CONTACT_DATA)
45
+ content = DEFAULT_CONTACT_DATA
46
+ else:
47
+ return jsonify({"msg": "Page not found"}), 404
48
+ # Ensure _id is a string if it's an ObjectId
49
+ if '_id' in content and not isinstance(content['_id'], str):
50
+ content['_id'] = str(content['_id'])
51
+ return jsonify(content)
52
+
53
+ @page_bp.route('/update', methods=['POST'])
54
+ def update_page_content():
55
+ """Handles form submission from the /update UI to save changes."""
56
+ page_name = request.form.get('page_name')
57
+ if page_name == 'about':
58
+ paragraphs = request.form.get('paragraphs', '').strip().split('\n')
59
+ paragraphs = [p.strip() for p in paragraphs if p.strip()]
60
+
61
+ update_data = {
62
+ "title": request.form.get('title'),
63
+ "slogan": request.form.get('slogan'),
64
+ "paragraphs": paragraphs
65
+ }
66
+ elif page_name == 'contact':
67
+ update_data = {
68
+ "title": request.form.get('title'),
69
+ "intro": request.form.get('intro'),
70
+ "details": [
71
+ {"type": "Phone Support", "value": request.form.get('phone_value')},
72
+ {"type": "Email Support", "value": request.form.get('email_value')},
73
+ {"type": "Business Hours", "value": request.form.get('hours_value')},
74
+ {"type": "Mailing Address", "value": request.form.get('address_value')}
75
+ ]
76
+ }
77
+ else:
78
+ return redirect('/api/update')
79
+
80
+ mongo.db.pages.update_one(
81
+ {'_id': page_name},
82
+ {'$set': update_data},
83
+ upsert=True
84
+ )
85
+ return redirect('/api/update')
86
+
87
+ @page_bp.route('/update_ui', methods=['GET'])
88
+ def update_ui():
89
+ """Serves the simple HTML UI for editing page content."""
90
+ # Note: Route changed to /update_ui to avoid conflict with /pages/update
91
+ about_data = mongo.db.pages.find_one({'_id': 'about'}) or DEFAULT_ABOUT_DATA
92
+ contact_data = mongo.db.pages.find_one({'_id': 'contact'}) or DEFAULT_CONTACT_DATA
93
+
94
+ about_paragraphs_text = "\n".join(about_data.get('paragraphs', []))
95
+ contact_details = {item['type']: item['value'] for item in contact_data.get('details', [])}
96
+
97
+ html = f"""
98
+ <!DOCTYPE html>
99
+ <html lang="en">
100
+ <head>
101
+ <meta charset="UTF-8">
102
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
103
+ <title>Update Page Content</title>
104
+ <style>
105
+ body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 2em; background-color: #f8f9fa; color: #212529; }}
106
+ .container {{ max-width: 800px; margin: auto; }}
107
+ h1, h2 {{ color: #343a40; border-bottom: 2px solid #dee2e6; padding-bottom: 0.5em; }}
108
+ form {{ background: white; padding: 2em; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.05); margin-bottom: 2em; }}
109
+ label {{ display: block; margin-top: 1em; margin-bottom: 0.5em; font-weight: bold; color: #495057; }}
110
+ input[type="text"], textarea {{ width: 100%; padding: 0.8em; border: 1px solid #ced4da; border-radius: 4px; box-sizing: border-box; font-size: 1rem; }}
111
+ textarea {{ height: 250px; resize: vertical; }}
112
+ button {{ background-color: #007bff; color: white; padding: 0.8em 1.5em; border: none; border-radius: 4px; cursor: pointer; font-size: 1em; font-weight: bold; }}
113
+ button:hover {{ background-color: #0056b3; }}
114
+ </style>
115
+ </head>
116
+ <body>
117
+ <div class="container">
118
+ <h1>Update Website Content</h1>
119
+
120
+ <!-- About Us Page Form -->
121
+ <h2>About Us Page</h2>
122
+ <form action="/api/pages/update" method="post">
123
+ <input type="hidden" name="page_name" value="about">
124
+ <label for="about_title">Title:</label>
125
+ <input type="text" id="about_title" name="title" value="{about_data.get('title', '')}">
126
+
127
+ <label for="about_slogan">Slogan:</label>
128
+ <input type="text" id="about_slogan" name="slogan" value="{about_data.get('slogan', '')}">
129
+
130
+ <label for="about_paragraphs">Paragraphs (one paragraph per line):</label>
131
+ <textarea id="about_paragraphs" name="paragraphs">{about_paragraphs_text}</textarea>
132
+
133
+ <br><br>
134
+ <button type="submit">Update About Page</button>
135
+ </form>
136
+
137
+ <!-- Contact Page Form -->
138
+ <h2>Customer Care Page</h2>
139
+ <form action="/api/pages/update" method="post">
140
+ <input type="hidden" name="page_name" value="contact">
141
+
142
+ <label for="contact_title">Title:</label>
143
+ <input type="text" id="contact_title" name="title" value="{contact_data.get('title', '')}">
144
+
145
+ <label for="contact_intro">Intro Text:</label>
146
+ <input type="text" id="contact_intro" name="intro" value="{contact_data.get('intro', '')}">
147
+
148
+ <label for="phone_value">Phone Support:</label>
149
+ <input type="text" id="phone_value" name="phone_value" value="{contact_details.get('Phone Support', '')}">
150
+
151
+ <label for="email_value">Email Support:</label>
152
+ <input type="text" id="email_value" name="email_value" value="{contact_details.get('Email Support', '')}">
153
+
154
+ <label for="hours_value">Business Hours:</label>
155
+ <input type="text" id="hours_value" name="hours_value" value="{contact_details.get('Business Hours', '')}">
156
+
157
+ <label for="address_value">Mailing Address:</label>
158
+ <input type="text" id="address_value" name="address_value" value="{contact_details.get('Mailing Address', '')}">
159
+
160
+ <br><br>
161
+ <button type="submit">Update Contact Page</button>
162
+ </form>
163
+ </div>
164
+ </body>
165
+ </html>
166
+ """
167
+ return html
app/xero_utils.py CHANGED
@@ -269,5 +269,4 @@ def trigger_po_creation(order_details):
269
  thread.daemon = True
270
  thread.start()
271
  except Exception as e:
272
- logger.error("Failed to start Xero PO creation thread for order %s. Error: %s", order_details.get('order_id'), e)
273
-
 
269
  thread.daemon = True
270
  thread.start()
271
  except Exception as e:
272
+ logger.error("Failed to start Xero PO creation thread for order %s. Error: %s", order_details.get('order_id'), e)
 
flask_session/2029240f6d1128be89ddc32729463129 CHANGED
Binary files a/flask_session/2029240f6d1128be89ddc32729463129 and b/flask_session/2029240f6d1128be89ddc32729463129 differ
 
flask_session/aa71dde20eaf768ca7e5f90a25563ea6 ADDED
Binary file (179 Bytes). View file
 
flask_session/d0efef4e2c8720847d7a47ac360d9d46 ADDED
Binary file (153 Bytes). View file
 
twillio_gemini_api.py CHANGED
@@ -1,56 +1,155 @@
1
  # app.py
2
 
3
  import os
 
4
  from flask import Flask, request
5
  from dotenv import load_dotenv
6
- import google.generativeai as genai
 
 
7
  from twilio.twiml.messaging_response import MessagingResponse
8
 
9
- # Load environment variables from .env file
10
- load_dotenv()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  # --- Initialize Flask App and APIs ---
 
 
 
 
13
  app = Flask(__name__)
14
 
15
- # Configure the Gemini API
16
  try:
17
- genai.configure(api_key=os.environ.get("GEMINI_API_KEY"))
18
- # Initialize the Gemini-1.5-Pro model
19
- # Use 'gemini-1.5-pro-latest' for the most recent version
20
- model = genai.GenerativeModel('gemini-1.5-pro-latest')
21
- print("Gemini model initialized successfully.")
22
  except Exception as e:
23
- print(f"Error initializing Gemini model: {e}")
24
- model = None
 
25
 
26
  # --- Define the Webhook Endpoint ---
27
  @app.route("/sms", methods=['POST'])
28
  def sms_reply():
29
- """Respond to incoming messages with a Gemini-generated response."""
30
 
31
- # Get the message body from the incoming Twilio request
32
- incoming_msg = request.values.get('Body', '').strip()
33
- print(f"Received message: '{incoming_msg}'")
34
-
35
- # Start the TwiML response
36
  twilio_resp = MessagingResponse()
37
 
38
- if not model:
39
- twilio_resp.message("The AI model is not configured correctly. Please check the server logs.")
40
  return str(twilio_resp)
41
 
42
- if not incoming_msg:
43
- twilio_resp.message("Please send a message to get a response.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  return str(twilio_resp)
45
 
46
  try:
47
- # Send the message to Gemini API and get the response
48
- print("Sending message to Gemini...")
49
- gemini_response = model.generate_content(incoming_msg)
 
 
 
 
 
 
 
 
50
 
51
- # Extract the text part of the response
52
  ai_answer = gemini_response.text
53
- print(f"Gemini response: '{ai_answer}'")
54
 
55
  # Add the Gemini response to the TwiML message
56
  twilio_resp.message(ai_answer)
@@ -61,12 +160,9 @@ def sms_reply():
61
  error_message = "Sorry, I'm having trouble connecting to my brain right now. Please try again later."
62
  twilio_resp.message(error_message)
63
 
64
- # Return the TwiML response as a string
65
  return str(twilio_resp)
66
 
67
 
68
  # --- Run the Flask App ---
69
  if __name__ == "__main__":
70
- # The debug=True option is for development only.
71
- # In production, use a proper WSGI server like Gunicorn or Waitress.
72
  app.run(debug=True, port=5000)
 
1
  # app.py
2
 
3
  import os
4
+ import requests # To download the audio file from Twilio's URL
5
  from flask import Flask, request
6
  from dotenv import load_dotenv
7
+ # Use the new top-level import and types
8
+ from google import genai
9
+ from google.genai import types
10
  from twilio.twiml.messaging_response import MessagingResponse
11
 
12
+ # --- Define Calculator Functions for the Model to Use ---
13
+ # The SDK uses the function signature (name, parameters) and docstring
14
+ # to tell the model how and when to use these tools.
15
+
16
+ def add(a: float, b: float):
17
+ """
18
+ Adds two numbers together.
19
+
20
+ Args:
21
+ a: The first number.
22
+ b: The second number.
23
+ """
24
+ print(f"Tool Call: add(a={a}, b={b})")
25
+ return a + b
26
+
27
+ def subtract(a: float, b: float):
28
+ """
29
+ Subtracts the second number from the first.
30
+
31
+ Args:
32
+ a: The number to subtract from.
33
+ b: The number to subtract.
34
+ """
35
+ print(f"Tool Call: subtract(a={a}, b={b})")
36
+ return a - b
37
+
38
+ def multiply(a: float, b: float):
39
+ """
40
+ Multiplies two numbers.
41
+
42
+ Args:
43
+ a: The first number.
44
+ b: The second number.
45
+ """
46
+ print(f"Tool Call: multiply(a={a}, b={b})")
47
+ return a * b
48
+
49
+ def divide(a: float, b: float):
50
+ """
51
+ Divides the first number by the second.
52
+
53
+ Args:
54
+ a: The numerator.
55
+ b: The denominator.
56
+ """
57
+ print(f"Tool Call: divide(a={a}, b={b})")
58
+ if b == 0:
59
+ return "Error: Cannot divide by zero."
60
+ return a / b
61
+
62
+ # A list of all the functions the model can call
63
+ calculator_tools = [add, subtract, multiply, divide]
64
+
65
 
66
  # --- Initialize Flask App and APIs ---
67
+
68
+ # Load environment variables from .env file
69
+ load_dotenv(r"C:\Users\Vaibhav Arora\Documents\MyExperimentsandCodes\APPS_WEBSITES\CANADA_WHOLESALE_PROJECT\GITHUB_REPOS\mvp-vue\wholesale-grocery-app\AIAPPS\.env")
70
+
71
  app = Flask(__name__)
72
 
73
+ # Configure the Gemini API and initialize the client
74
  try:
75
+ client = genai.Client(api_key=os.environ.get("GEMINI_API_KEY"))
76
+ print("Gemini client and calculator tools initialized successfully.")
 
 
 
77
  except Exception as e:
78
+ print(f"Error initializing Gemini client: {e}")
79
+ client = None
80
+
81
 
82
  # --- Define the Webhook Endpoint ---
83
  @app.route("/sms", methods=['POST'])
84
  def sms_reply():
85
+ """Respond to incoming text or audio messages, using calculator functions if needed."""
86
 
 
 
 
 
 
87
  twilio_resp = MessagingResponse()
88
 
89
+ if not client:
90
+ twilio_resp.message("The AI client is not configured correctly. Please check the server logs.")
91
  return str(twilio_resp)
92
 
93
+ # Prepare the contents list for the Gemini API call
94
+ contents = []
95
+
96
+ # Check if the incoming message contains media
97
+ num_media = int(request.values.get('NumMedia', 0))
98
+
99
+ if num_media > 0:
100
+ media_url = request.values.get('MediaUrl0')
101
+ mime_type = request.values.get('MediaContentType0')
102
+
103
+ # Process only if the media is audio
104
+ if 'audio' in mime_type:
105
+ print(f"Received audio message. URL: {media_url}, MIME Type: {mime_type}")
106
+
107
+ # Download the audio file from the Twilio URL
108
+ audio_response = requests.get(media_url)
109
+
110
+ if audio_response.status_code == 200:
111
+ audio_bytes = audio_response.content
112
+ audio_part = types.Part.from_bytes(data=audio_bytes, mime_type=mime_type)
113
+ prompt = "Please transcribe this audio. If it contains a calculation or a question, please answer it."
114
+ contents = [prompt, audio_part]
115
+ else:
116
+ error_message = "Sorry, I couldn't download the audio file to process it. Please try again."
117
+ twilio_resp.message(error_message)
118
+ return str(twilio_resp)
119
+ else:
120
+ # Handle non-audio media like images or videos
121
+ twilio_resp.message("Sorry, I can only process text and audio messages.")
122
+ return str(twilio_resp)
123
+
124
+ else:
125
+ # Fallback to text message processing
126
+ incoming_msg = request.values.get('Body', '').strip()
127
+ print(f"Received text message: '{incoming_msg}'")
128
+ if not incoming_msg:
129
+ twilio_resp.message("Please send a text or audio message to get a response.")
130
+ return str(twilio_resp)
131
+ contents = [incoming_msg]
132
+
133
+ if not contents:
134
+ twilio_resp.message("Could not determine content to process. Please send a message.")
135
  return str(twilio_resp)
136
 
137
  try:
138
+ print("Sending content to Gemini with calculator tools...")
139
+
140
+ # Configure the request to use our calculator functions
141
+ config = types.GenerateContentConfig(tools=calculator_tools)
142
+
143
+ # The SDK handles the multi-turn process for function calling automatically
144
+ gemini_response = client.models.generate_content(
145
+ model="gemini-2.5-flash",
146
+ contents=contents, # This will contain either text or [prompt, audio_part]
147
+ config=config,
148
+ )
149
 
150
+ # This is the final text answer after any function calls have been resolved.
151
  ai_answer = gemini_response.text
152
+ print(f"Gemini final response: '{ai_answer}'")
153
 
154
  # Add the Gemini response to the TwiML message
155
  twilio_resp.message(ai_answer)
 
160
  error_message = "Sorry, I'm having trouble connecting to my brain right now. Please try again later."
161
  twilio_resp.message(error_message)
162
 
 
163
  return str(twilio_resp)
164
 
165
 
166
  # --- Run the Flask App ---
167
  if __name__ == "__main__":
 
 
168
  app.run(debug=True, port=5000)