akiko19191 commited on
Commit
72eef4f
·
verified ·
1 Parent(s): bc54033

Upload folder using huggingface_hub

Browse files
Files changed (46) hide show
  1. Dockerfile +17 -0
  2. __pycache__/search_engine.cpython-311.pyc +0 -0
  3. __pycache__/utils.cpython-311.pyc +0 -0
  4. app/Company_Info.py +29 -0
  5. app/__init__.py +66 -0
  6. app/__pycache__/Company_Info.cpython-311.pyc +0 -0
  7. app/__pycache__/__init__.cpython-311.pyc +0 -0
  8. app/__pycache__/ai_features.cpython-311.pyc +0 -0
  9. app/__pycache__/api.cpython-311.pyc +0 -0
  10. app/__pycache__/config.cpython-311.pyc +0 -0
  11. app/__pycache__/email_utils.cpython-311.pyc +0 -0
  12. app/__pycache__/extensions.cpython-311.pyc +0 -0
  13. app/__pycache__/general_utils.cpython-311.pyc +0 -0
  14. app/__pycache__/models.cpython-311.pyc +0 -0
  15. app/__pycache__/page_features.cpython-311.pyc +0 -0
  16. app/__pycache__/xero_client.cpython-311.pyc +0 -0
  17. app/__pycache__/xero_routes.cpython-311.pyc +0 -0
  18. app/__pycache__/xero_utils.cpython-311.pyc +0 -0
  19. app/ai_features.py +697 -0
  20. app/api.py +392 -0
  21. app/config.py +23 -0
  22. app/email_utils.py +214 -0
  23. app/extensions.py +13 -0
  24. app/general_utils.py +70 -0
  25. app/models.py +7 -0
  26. app/page_features.py +167 -0
  27. app/templates/base.html +16 -0
  28. app/templates/code.html +16 -0
  29. app/xero_client.py +66 -0
  30. app/xero_routes.py +104 -0
  31. app/xero_utils.py +346 -0
  32. cache/.gitkeep +0 -0
  33. cache/2029240f6d1128be89ddc32729463129 +0 -0
  34. cache/7b19acab5f13885d95f709e2dcbd5729 +0 -0
  35. default_settings.py +11 -0
  36. flask_session/2029240f6d1128be89ddc32729463129 +0 -0
  37. flask_session/5dd30f329f4495f0adf98d75dcf44c99 +0 -0
  38. flask_session/aa71dde20eaf768ca7e5f90a25563ea6 +0 -0
  39. flask_session/d0efef4e2c8720847d7a47ac360d9d46 +0 -0
  40. hfapis.py +25 -0
  41. logging_settings.py +26 -0
  42. requirements.txt +16 -0
  43. run.py +7 -0
  44. search_engine.py +90 -0
  45. twillio_gemini_api.py +168 -0
  46. utils.py +30 -0
Dockerfile ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11
2
+
3
+ RUN useradd -m -u 1000 user
4
+ USER user
5
+ ENV PATH="/home/user/.local/bin:$PATH"
6
+
7
+ WORKDIR /app
8
+
9
+ COPY --chown=user ./requirements.txt requirements.txt
10
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
11
+
12
+ COPY --chown=user . /app
13
+ EXPOSE 7860
14
+ #ENTRYPOINT ["python"]
15
+ WORKDIR /app
16
+
17
+ ENTRYPOINT ["python","run.py"]
__pycache__/search_engine.cpython-311.pyc ADDED
Binary file (4.8 kB). View file
 
__pycache__/utils.cpython-311.pyc ADDED
Binary file (2.16 kB). View file
 
app/Company_Info.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ company_info="""
2
+ ### **Detailed Summary**
3
+
4
+ **1. Company Profile:**
5
+ * **Name:** Matax Express Ltd.
6
+ * **Business Type:** Wholesale produce provider.
7
+ * **Products:** Premium, fresh fruits and vegetables.
8
+ * **Experience:** Over 30 years serving the community.
9
+ * **Service Area:** Greater Toronto Area (GTA).
10
+
11
+ **2. Target Audience:**
12
+ * The company is a B2B (Business-to-Business) supplier.
13
+ * Their clients include **restaurants, local markets, and retail stores.**
14
+
15
+ **3. Key Selling Points & Commitments:**
16
+ * **Quality:** They promise "consistent quality" with "premium fruits and vegetables."
17
+ * **Price:** They claim to offer "unbeaten prices."
18
+ * **Service & Reliability:** This is their core focus. They emphasize "GREAT SERVICE," reliable deliveries, and personalized support. They aim to be a "daily, trusted partner" rather than just a supplier.
19
+ * **Convenience:** They offer delivery **7 days a week** directly to a client's business.
20
+ * **Customer-Focused:** They value customer feedback for continuous improvement and strive to make partnerships "seamless and efficient."
21
+
22
+ **4. Contact Information & Customer Care:**
23
+ * **Response Time:** They aim to respond to all inquiries within 24 business hours.
24
+ * **Phone Support:** +1 (800) 123-4567
25
+ * **Email Support:** support@mataxexpress.com
26
+ * **Business Hours:** Monday - Friday, 9:00 AM - 5:00 PM (EST)
27
+ * **Mailing Address:** 123 Fresh Produce Lane, Farmville, ST 54321
28
+ * **Social Media:** They can be found at the handle **@MataxExpress**.
29
+ """
app/__init__.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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)
20
+
21
+ # Initialize extensions with the app instance
22
+ mongo.init_app(app)
23
+ bcrypt.init_app(app)
24
+ jwt.init_app(app)
25
+ cors.init_app(app, supports_credentials=True)
26
+ session.init_app(app)
27
+ oauth.init_app(app)
28
+
29
+ # Manually configure client credentials after the app config is loaded
30
+ client_id = app.config.get('CLIENT_ID')
31
+ client_secret = app.config.get('CLIENT_SECRET')
32
+
33
+ if client_id and client_secret:
34
+ # 1. Configure the flask-oauthlib remote app
35
+ xero.client_id = client_id
36
+ xero.client_secret = client_secret
37
+
38
+ # 2. Configure the xero-python SDK api_client
39
+ api_client.configuration.debug = app.config['DEBUG']
40
+ api_client.configuration.oauth2_token.client_id = client_id
41
+ api_client.configuration.oauth2_token.client_secret = client_secret
42
+ else:
43
+ logging.warning("Xero CLIENT_ID and/or CLIENT_SECRET are not configured.")
44
+
45
+
46
+ # Register JWT error handlers
47
+ @jwt.unauthorized_loader
48
+ def unauthorized_callback(reason):
49
+ return jsonify(message="Missing Authorization Header"), 401
50
+
51
+ @jwt.invalid_token_loader
52
+ def invalid_token_callback(error):
53
+ return jsonify(message="Signature verification failed. Token is invalid."), 401
54
+
55
+ @jwt.expired_token_loader
56
+ def expired_token_callback(jwt_header, jwt_payload):
57
+ return jsonify(message="Token has expired"), 401
58
+
59
+ # Register blueprints to organize routes
60
+ from .api import api_bp
61
+ from .xero_routes import xero_bp
62
+
63
+ app.register_blueprint(api_bp, url_prefix='/api')
64
+ app.register_blueprint(xero_bp)
65
+
66
+ return app
app/__pycache__/Company_Info.cpython-311.pyc ADDED
Binary file (1.8 kB). View file
 
app/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (3.83 kB). View file
 
app/__pycache__/ai_features.cpython-311.pyc ADDED
Binary file (44.5 kB). View file
 
app/__pycache__/api.cpython-311.pyc ADDED
Binary file (27.7 kB). View file
 
app/__pycache__/config.cpython-311.pyc ADDED
Binary file (1.4 kB). View file
 
app/__pycache__/email_utils.cpython-311.pyc ADDED
Binary file (10.9 kB). View file
 
app/__pycache__/extensions.cpython-311.pyc ADDED
Binary file (875 Bytes). View file
 
app/__pycache__/general_utils.cpython-311.pyc ADDED
Binary file (3.31 kB). View file
 
app/__pycache__/models.cpython-311.pyc ADDED
Binary file (642 Bytes). View file
 
app/__pycache__/page_features.cpython-311.pyc ADDED
Binary file (11.1 kB). View file
 
app/__pycache__/xero_client.cpython-311.pyc ADDED
Binary file (4.31 kB). View file
 
app/__pycache__/xero_routes.cpython-311.pyc ADDED
Binary file (8.52 kB). View file
 
app/__pycache__/xero_utils.cpython-311.pyc ADDED
Binary file (17.1 kB). View file
 
app/ai_features.py ADDED
@@ -0,0 +1,697 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #your_app/ai_features.py
2
+
3
+
4
+ import re
5
+ from flask import Blueprint, request, jsonify, current_app
6
+ from bson.objectid import ObjectId
7
+ from datetime import datetime, date, timedelta
8
+ from flask_jwt_extended import jwt_required, get_jwt_identity
9
+ from .extensions import bcrypt, mongo
10
+ from .Company_Info import company_info
11
+ from .general_utils import backfill_orders,get_next_order_serial
12
+ from google import genai
13
+ from google.genai import types
14
+ import os
15
+ import json
16
+ import traceback
17
+ from .xero_utils import trigger_po_creation,trigger_contact_creation
18
+ # +++ START: WHATSAPP FEATURE IMPORTS +++
19
+ import requests
20
+ from twilio.twiml.messaging_response import MessagingResponse
21
+ # +++ END: WHATSAPP FEATURE IMPORTS +++
22
+
23
+ ai_bp = Blueprint('ai', __name__)
24
+
25
+ today = date.today()
26
+ weekday_number = today.weekday()
27
+ days_of_week = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
28
+ weekday_name = days_of_week[weekday_number]
29
+ current_date = datetime.now().strftime('%Y-%m-%d')
30
+
31
+
32
+
33
+ # MODIFIED
34
+ add_items_to_cart_function = {
35
+ "name": "add_items_to_cart",
36
+ "description": "Extracts order details from a message to add products to a specific customer's shopping cart. Use this to handle orders like 'Mr Vaibhav Arora Tomato 1 Case Apples 2 Bags'.",
37
+ "parameters": {
38
+ "type": "object", "properties": {
39
+ "user_identifier": {
40
+ "type": "string",
41
+ "description": "The name, email, or business name of the customer for whom the order is being placed. This MUST be extracted from the message."
42
+ },
43
+ "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., 'case', 'bag', 'piece').This must match one of the available units for the product.Must be lowercase, Eg: The correct unit is:`bag` , and invalid unit is `bags`", }, }, "required": ["product_name", "quantity", "unit"], }, },
44
+ "delivery_date": {
45
+ "type": "string",
46
+ "description": "The desired delivery date for the order, in YYYY-MM-DD format. Extract if the user explicitly states a date.",
47
+ }
48
+ },
49
+ "required": ["user_identifier", "items"],
50
+ },
51
+ }
52
+
53
+ # MODIFIED
54
+ remove_items_from_cart_function = {
55
+ "name": "remove_items_from_cart",
56
+ "description": "Removes one or more specific items from a customer's shopping cart based on their name.",
57
+ "parameters": {
58
+ "type": "object",
59
+ "properties": {
60
+ "user_identifier": {
61
+ "type": "string",
62
+ "description": "The name, email, or business name of the customer. Must be extracted from the user's message."
63
+ },
64
+ "items": {
65
+ "type": "array",
66
+ "description": "A list of items to remove from the cart. Only product_name is required.",
67
+ "items": {
68
+ "type": "object",
69
+ "properties": {
70
+ "product_name": {
71
+ "type": "string",
72
+ "description": "The name of the product to remove. This must match an available product name.",
73
+ },
74
+ },
75
+ "required": ["product_name"],
76
+ },
77
+ }
78
+ },
79
+ "required": ["user_identifier", "items"],
80
+ },
81
+ }
82
+
83
+ # MODIFIED
84
+ clear_cart_function = {
85
+ "name": "clear_cart",
86
+ "description": "Removes all items from a specific customer's shopping cart.",
87
+ "parameters": {
88
+ "type": "object",
89
+ "properties": {
90
+ "user_identifier": {
91
+ "type": "string",
92
+ "description": "The name, email, or business name of the customer whose cart should be cleared. Must be extracted from the user's message."
93
+ }
94
+ },
95
+ "required": ["user_identifier"]
96
+ }
97
+ }
98
+
99
+ # +++ START: NEW FUNCTION DEFINITION FOR DIRECT ORDER +++
100
+ create_direct_order_function = {
101
+ "name": "create_direct_order",
102
+ "description": "Parses a complete order from a single message and places it directly, bypassing the cart. Use this when the user provides the customer name, items, and a delivery date all at once.",
103
+ "parameters": {
104
+ "type": "object",
105
+ "properties": {
106
+ "user_identifier": {
107
+ "type": "string",
108
+ "description": "The name, email, or business name of the customer for whom the order is being placed. This MUST be extracted from the message."
109
+ },
110
+ "items": {
111
+ "type": "array",
112
+ "description": "A list of items for the order.",
113
+ "items": {
114
+ "type": "object",
115
+ "properties": {
116
+ "product_name": {"type": "string", "description": "The name of the product."},
117
+ "quantity": {"type": "number", "description": "The quantity of the product."},
118
+ "unit": {"type": "string", "description": "The unit of measure (e.g., 'case', 'bag', 'piece').Must be lowercase"}
119
+ },
120
+ "required": ["product_name", "quantity", "unit"]
121
+ }
122
+ },
123
+ "delivery_date": {
124
+ "type": "string",
125
+ "description": "The desired delivery date for the order, in YYYY-MM-DD format. This is required to use this function."
126
+ },
127
+ "additional_info": {
128
+ "type": "string",
129
+ "description": "Any special instructions or notes from the user for the delivery."
130
+ }
131
+ },
132
+ "required": ["user_identifier", "items", "delivery_date"],
133
+ },
134
+ }
135
+
136
+ navigate_to_page_function = {
137
+ "name": "navigate_to_page",
138
+ "description": "Provides a button in the chat to navigate the user to a specific page.",
139
+ "parameters": {
140
+ "type": "object",
141
+ "properties": {
142
+ "page": {
143
+ "type": "string",
144
+ "description": "The destination page. Must be one of the allowed pages.",
145
+ "enum": ["orders", "cart", "catalog", "home", "about", "care", "login", "register"]
146
+ }
147
+ },
148
+ "required": ["page"]
149
+ }
150
+ }
151
+
152
+ # MODIFIED
153
+ get_my_orders_function = {
154
+ "name": "get_my_orders",
155
+ "description": "Retrieves a summary of a specific customer's most recent orders to display in the chat.",
156
+ "parameters": {
157
+ "type": "object",
158
+ "properties": {
159
+ "user_identifier": {
160
+ "type": "string",
161
+ "description": "The name, email, or business name of the customer whose orders are being requested."
162
+ }
163
+ },
164
+ "required": ["user_identifier"]
165
+ }
166
+ }
167
+
168
+ # MODIFIED
169
+ get_cart_items_function = {
170
+ "name": "get_cart_items",
171
+ "description": "Retrieves the items currently in a specific customer's shopping cart and lists them.",
172
+ "parameters": {
173
+ "type": "object",
174
+ "properties": {
175
+ "user_identifier": {
176
+ "type": "string",
177
+ "description": "The name, email, or business name of the customer whose cart is being viewed."
178
+ }
179
+ },
180
+ "required": ["user_identifier"]
181
+ }
182
+ }
183
+
184
+ get_order_details_function = {
185
+ "name": "get_order_details",
186
+ "description": "Retrieves the specific items and details for a single order based on its ID.",
187
+ "parameters": {
188
+ "type": "object",
189
+ "properties": {
190
+ "order_id": {
191
+ "type": "string",
192
+ "description": "The ID of the order to fetch. Can be the full ID or the last 6 characters."
193
+ }
194
+ },
195
+ "required": ["order_id"]
196
+ }
197
+ }
198
+
199
+ cancel_order_function = {
200
+ "name": "cancel_order",
201
+ "description": "Proposes to cancel an order based on its ID. This requires user confirmation.",
202
+ "parameters": {
203
+ "type": "object",
204
+ "properties": {
205
+ "order_id": {
206
+ "type": "string",
207
+ "description": "The ID of the order to be cancelled."
208
+ }
209
+ },
210
+ "required": ["order_id"]
211
+ }
212
+ }
213
+
214
+ # MODIFIED
215
+ place_order_function = {
216
+ "name": "place_order",
217
+ "description": "Places a final order for a customer using the items in their cart. This action is final and will clear the cart.",
218
+ "parameters": {
219
+ "type": "object",
220
+ "properties": {
221
+ "user_identifier": {
222
+ "type": "string",
223
+ "description": "The name, email, or business name of the customer for whom the order is being placed. Must be extracted from the user's message."
224
+ },
225
+ "delivery_date": {
226
+ "type": "string",
227
+ "description": "The desired delivery date in YYYY-MM-DD format. Must be confirmed with the user."
228
+ },
229
+ "additional_info": {
230
+ "type": "string",
231
+ "description": "Any special instructions or notes from the user for the delivery."
232
+ }
233
+ },
234
+ "required": ["user_identifier", "delivery_date"]
235
+ }
236
+ }
237
+
238
+ register_new_customer_function = {
239
+ "name": "register_new_customer",
240
+ "description": "Registers a new customer by collecting their essential details. Use this when a user expresses intent to sign up or is not recognized.",
241
+ "parameters": {
242
+ "type": "object",
243
+ "properties": {
244
+ "businessName": {
245
+ "type": "string",
246
+ "description": "The customer's business or company name."
247
+ },
248
+ "email": {
249
+ "type": "string",
250
+ "description": "The customer's primary email address. This will be their login username."
251
+ },
252
+ "password": {
253
+ "type": "string",
254
+ "description": "A password for the user's account. The user must provide this."
255
+ },
256
+ "phoneNumber": {
257
+ "type": "string",
258
+ "description": "The user's contact phone number."
259
+ },
260
+ "businessAddress": {
261
+ "type": "string",
262
+ "description": "The full delivery address for the business."
263
+ },
264
+ "contactPerson": {
265
+ "type": "string",
266
+ "description": "The customer's full name or primary contact person's name."
267
+ }
268
+ },
269
+ "required": ["businessName", "email", "password", "phoneNumber", "businessAddress"]
270
+ }
271
+ }
272
+
273
+ get_my_orders_function_website = {
274
+ "name": "get_my_orders",
275
+ "description": "Retrieves a summary of the user's most recent orders to display in the chat.",
276
+ "parameters": {"type": "object", "properties": {}}
277
+ }
278
+
279
+ add_items_to_cart_function_website = {
280
+ "name": "add_items_to_cart",
281
+ "description": "Extracts order details from a user's message to add products to their shopping cart. Can also set a delivery date.",
282
+ "parameters": {
283
+ "type": "object", "properties": {
284
+ "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"], }, },
285
+ "delivery_date": {
286
+ "type": "string",
287
+ "description": "The desired delivery date for the order, in YYYY-MM-DD format. The user must explicitly state a date.",
288
+ }
289
+ },
290
+ "required": ["items"],
291
+ },
292
+ }
293
+ cancel_order_function_website = {
294
+ "name": "cancel_order",
295
+ "description": "Proposes to cancel an order based on its ID. This requires user confirmation.",
296
+ "parameters": {
297
+ "type": "object",
298
+ "properties": {
299
+ "order_id": {
300
+ "type": "string",
301
+ "description": "The ID of the order to be cancelled. Can be the full ID or the last 6 characters."
302
+ }
303
+ },
304
+ "required": ["order_id"]
305
+ }
306
+ }
307
+
308
+ @ai_bp.route('/chat', methods=['POST'])
309
+ @jwt_required()
310
+ def handle_ai_chat():
311
+
312
+ # ... (Web chat endpoint remains unchanged)
313
+ user_email = get_jwt_identity()
314
+ MODES = ['case', 'bag', 'piece', 'tray', 'weight']
315
+ all_products = list(mongo.db.products.find({}, {'name': 1, 'unit': 1, '_id': 0}))
316
+ product_context_list = []
317
+ for p in all_products:
318
+ if 'name' not in p: continue
319
+ unit_string = p.get('unit', '').lower()
320
+ available_modes = [mode for mode in MODES if mode in unit_string]
321
+ if available_modes:
322
+ product_context_list.append(f"{p['name']} (available units: {', '.join(available_modes)})")
323
+ else:
324
+ product_context_list.append(p['name'])
325
+ client = genai.Client(api_key="AIzaSyB7yKIdfW7Umv62G47BCdJjoHTJ9TeiAko")
326
+
327
+ min_delivery_date = (datetime.now() + timedelta(days=2)).strftime('%Y-%m-%d')
328
+
329
+ tools = types.Tool(function_declarations=[
330
+ add_items_to_cart_function_website,
331
+ navigate_to_page_function,
332
+ get_my_orders_function_website,
333
+ cancel_order_function_website,
334
+ ])
335
+ config = types.GenerateContentConfig(tools=[tools])
336
+
337
+ instruction_prompt = f"""
338
+ You are an expert assistant for a food wholesale company. Your capabilities include:
339
+ 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.
340
+ 2. Product List: {'; '.join(product_context_list)}.
341
+ 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.
342
+ 4. Order Summary: Use `get_my_orders` to LIST a summary of recent orders in the chat.
343
+ 5. Order Cancellation: Use `cancel_order` to start the cancellation process for a SPECIFIC order. The user must provide an order ID.
344
+ 6. New Customer Registration: Use `register_new_customer` if a user wants to sign up.
345
+ 7. The Capability You must use AT ALL TIMES: Luring the customer to place an order and persuading them to buy more products.
346
+ *Always Brag about the quality of our products and our customer service and try to persuade the customer to place an order.*
347
+ Company Info for general questions: {company_info}
348
+ *** 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. ***
349
+ Always refer to the chat history for context. **If information is missing, ask for it.**
350
+ """
351
+
352
+ user_input_part, history_data = None, []
353
+ if 'multipart/form-data' in request.content_type:
354
+ audio_file = request.files.get('audio')
355
+ history_json = request.form.get('history', '[]')
356
+ history_data = json.loads(history_json)
357
+ user_input_part = types.Part.from_bytes(data=audio_file.read(), mime_type=audio_file.mimetype.split(';')[0])
358
+ elif 'application/json' in request.content_type:
359
+ json_data = request.get_json()
360
+ history_data = json_data.get('history', [])
361
+ user_input_part = json_data.get('message')
362
+ else: return jsonify({"msg": "Unsupported format"}), 415
363
+
364
+ history_contents = [msg.get('data') for msg in history_data if msg.get('type') == 'text']
365
+ final_contents = [instruction_prompt] + history_contents + [user_input_part]
366
+ response = client.models.generate_content(model="gemini-2.5-flash", contents=final_contents, config=config)
367
+ response_part = response.candidates[0].content.parts[0]
368
+
369
+ if response_part.function_call:
370
+ backfill_orders()
371
+ function_call = response_part.function_call
372
+
373
+ if function_call.name == "add_items_to_cart":
374
+ proposal_data = {
375
+ "items": function_call.args.get('items', []),
376
+ "delivery_date": function_call.args.get('delivery_date')
377
+ }
378
+ return jsonify({"type": "order_proposal", "data": proposal_data}), 200
379
+
380
+ elif function_call.name == "register_new_customer":
381
+ return jsonify({
382
+ "type": "navigation_proposal",
383
+ "data": {
384
+ "path": "/register",
385
+ "button_text": "Go to Sign Up Page",
386
+ "prompt_text": "Excellent! I can help with that. Please click the button below to go to our registration page."
387
+ }
388
+ }), 200
389
+
390
+ elif function_call.name == "navigate_to_page":
391
+ page = function_call.args.get('page')
392
+ path_map = {'orders': '/order', 'cart': '/cart', 'catalog': '/catalog', 'home': '/', 'about': '/about', 'care': '/care', 'login': '/login', 'register': '/register'}
393
+ 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'}
394
+ if page in path_map:
395
+ 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
396
+
397
+ elif function_call.name == "get_my_orders":
398
+ recent_orders = list(mongo.db.orders.find({'user_email': user_email}).sort('created_at', -1).limit(5))
399
+ if not recent_orders: return jsonify({"type": "text", "data": "You have no recent orders."}), 200
400
+ details_text=""
401
+ for order in recent_orders:
402
+ product_ids = [ObjectId(item['productId']) for item in order.get('items', [])]
403
+ products_map = {str(p['_id']): p for p in mongo.db.products.find({'_id': {'$in': product_ids}})}
404
+ 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', [])]
405
+ details_text += f"\n\n **Details for Order #{str(order['serial_no'])}** _(Delivery Status: {order.get('status')})_:\n" + "\n".join(item_lines)
406
+ return jsonify({"type": "text", "data": "**Here are your recent orders**:\n" + details_text}), 200
407
+
408
+ elif function_call.name == "cancel_order":
409
+ order_id_frag = function_call.args.get("order_id", "").strip()
410
+ all_user_orders = mongo.db.orders.find({'user_email': user_email})
411
+ order = None
412
+ for o in all_user_orders:
413
+ if str(o['serial_no']) == order_id_frag:
414
+ order = o
415
+ break
416
+ if not order: return jsonify({"type": "text", "data": f"Sorry, I couldn't find an order with ID matching '{order_id_frag}'."}), 200
417
+ if order.get('status') not in ['pending', 'confirmed']: return jsonify({"type": "text", "data": f"Order #{order['serial_no']} cannot be cancelled as its status is '{order.get('status')}'."}), 200
418
+ return jsonify({
419
+ "type": "cancellation_proposal",
420
+ "data": {"order_id": str(order['_id']), "prompt_text": f"Are you sure you want to cancel order #{order['serial_no']}?"}
421
+ }), 200
422
+
423
+ return jsonify({"type": "text", "data": response.text}), 200
424
+
425
+ def _find_user_by_identifier(identifier):
426
+ """
427
+ Finds a user by contact person, business name, or email with improved matching.
428
+ Handles prefixes and prompts for clarification on multiple matches.
429
+ """
430
+ if not identifier or not isinstance(identifier, str):
431
+ return None, "No user identifier was provided. Please specify a customer."
432
+
433
+ # Clean the identifier to remove common titles for better name matching
434
+ titles = ['mr', 'mrs', 'ms', 'dr', 'mr.', 'mrs.']
435
+ original_identifier = identifier.strip()
436
+ # Clean the identifier for name searches by removing titles
437
+ cleaned_identifier = ' '.join([word for word in original_identifier.split() if word.lower().strip('.') not in titles])
438
+
439
+ # If cleaning results in an empty string (e.g., input was just "Mr."), use the original.
440
+ if not cleaned_identifier:
441
+ cleaned_identifier = original_identifier
442
+
443
+ query = {
444
+ "$or": [
445
+ # Search for the cleaned name within the contactPerson and businessName fields
446
+ {"contactPerson": {"$regex": re.escape(cleaned_identifier), "$options": "i"}},
447
+ {"businessName": {"$regex": re.escape(cleaned_identifier), "$options": "i"}},
448
+ # Use a more exact match for the email with the original input
449
+ {"email": {"$regex": f"^{re.escape(original_identifier)}$", "$options": "i"}}
450
+ ]
451
+ }
452
+ users_found = list(mongo.db.users.find(query))
453
+
454
+ if len(users_found) == 0:
455
+ return None, f"I could not find a customer matching '{identifier}'. Please check the name , or try finding by using their email id or register them as a new customer."
456
+
457
+ if len(users_found) > 1:
458
+ options_text = "*Did you mean:*\n"
459
+ for u in users_found:
460
+ contact_person = u.get('contactPerson', 'N/A')
461
+ business_name = u.get('businessName', 'N/A')
462
+ options_text += f"- {contact_person} (from {business_name})?\n"
463
+ options_text += "\nPlease clarify by providing the full name, business name, or email."
464
+ return None, options_text
465
+
466
+ return users_found[0], None # Return user object and no error message
467
+
468
+ @ai_bp.route('/whatsapp', methods=['POST'])
469
+ def whatsapp_reply():
470
+ """ Responds to incoming WhatsApp messages (text or audio) via Twilio. """
471
+ final_response_text = "I'm sorry, I encountered an error. Please try again."
472
+ twilio_resp = MessagingResponse()
473
+
474
+ try:
475
+ # --- 1. Get Message from Twilio Request ---
476
+ whatsapp_number = request.values.get('From')
477
+ user_message_text = request.values.get('Body', '').strip()
478
+ num_media = int(request.values.get('NumMedia', 0))
479
+
480
+ if not whatsapp_number:
481
+ current_app.logger.error("Request received without a 'From' number.")
482
+ twilio_resp.message("Could not identify your number. Please try again.")
483
+ return str(twilio_resp)
484
+
485
+ # --- 2. Setup AI Client and Product Context ---
486
+ client = genai.Client(api_key=os.getenv("GEMINI_API_KEY"))
487
+ MODES = ['case', 'bag', 'piece', 'tray', 'weight']
488
+ all_products = list(mongo.db.products.find({}, {'name': 1, 'unit': 1, '_id': 0}))
489
+ product_context_list = []
490
+ for p in all_products:
491
+ if 'name' not in p: continue
492
+ unit_string = p.get('unit', '').lower()
493
+ available_modes = [mode for mode in MODES if mode in unit_string]
494
+ if available_modes:
495
+ product_context_list.append(f"{p['name']} (available units: {', '.join(available_modes)})")
496
+ else:
497
+ product_context_list.append(p['name'])
498
+ # --- 3. Process User Input (Text or Audio) ---
499
+ user_input_part = None
500
+ if num_media > 0:
501
+ media_url = request.values.get('MediaUrl0')
502
+ mime_type = request.values.get('MediaContentType0')
503
+ if 'audio' in mime_type:
504
+ audio_response = requests.get(media_url)
505
+ if audio_response.status_code == 200:
506
+ user_input_part = types.Part.from_bytes(data=audio_response.content, mime_type=mime_type.split(';')[0])
507
+
508
+ if not user_input_part and user_message_text:
509
+ user_input_part = user_message_text
510
+
511
+ if not user_input_part:
512
+ twilio_resp.message("Please send a message or a voice note.")
513
+ return str(twilio_resp)
514
+
515
+ # --- 4. Configure AI for Multi-Customer Admin ---
516
+ history_doc = mongo.db.whatsapp_history.find_one({'whatsapp_number': whatsapp_number})
517
+ chat_history = history_doc.get('history', []) if history_doc else []
518
+
519
+ tools = types.Tool(function_declarations=[
520
+ create_direct_order_function, # ADDED
521
+ add_items_to_cart_function,
522
+ remove_items_from_cart_function,
523
+ clear_cart_function,
524
+ get_cart_items_function,
525
+ place_order_function,
526
+ get_my_orders_function,
527
+ register_new_customer_function
528
+ ])
529
+
530
+ instruction_prompt = f"""
531
+ The current date is {current_date,weekday_name}.
532
+ You are an expert wholesale ordering assistant for 'Matax Express', communicating via WhatsApp. You are talking to an admin user who will place orders and manage accounts for MULTIPLE different customers.
533
+
534
+ Your primary capabilities are:
535
+ 1. **Placing and Managing Orders for Customers:**
536
+ - The admin's message will specify the customer by their name (e.g., "Mr. Vaibhav Arora"), business name, or email.
537
+ - Your FIRST step is to extract this customer identifier. You MUST provide this `user_identifier` in all function calls related to carts or orders.
538
+ - If you cannot identify a customer, or if the name is ambiguous, you must ask the admin for clarification.
539
+
540
+ 2. **Direct Orders (One-Shot):**
541
+ - If the user provides a customer name, a list of items, AND a delivery date in a single message, use the `create_direct_order` function to place the order immediately, bypassing the cart.
542
+
543
+ 3. **Registering New Customers:**
544
+ - If the admin asks to register a new customer, use the `register_new_customer` function.
545
+
546
+ **Function Guide & Rules:**
547
+ - `create_direct_order`: Use for complete, one-shot orders with a delivery date.
548
+ - `add_items_to_cart`: To add items for a specific customer when NO delivery date is given.
549
+ - `place_order`: To finalize and place a customer's order *from their cart*.
550
+ - `register_new_customer`: To sign up a new customer.
551
+ - **Product List:** {', '.join(product_context_list)}.
552
+ - **Company Info:** For general questions: {company_info}.
553
+ - **Communication:** Always be professional. Confirm actions clearly. Respond using WhatsApp-compatible markdown (*bold*, _italic_).
554
+ - **Website Info:** When a customer is registered or an order is placed, inform the admin that the customer can log in at https://matax-express.vercel.app/.
555
+ """
556
+
557
+ config = types.GenerateContentConfig(tools=[tools])
558
+
559
+ final_contents = [instruction_prompt] + chat_history + [user_input_part]
560
+ response = client.models.generate_content(
561
+ model="gemini-2.5-flash", contents=final_contents, config=config
562
+ )
563
+
564
+ response_part = response.candidates[0].content.parts[0]
565
+
566
+ # --- 5. Process AI Response (Function Call or Text) ---
567
+ if response_part.function_call:
568
+ backfill_orders()
569
+ function_call = response_part.function_call
570
+ args = function_call.args
571
+
572
+ # Handle functions that don't need a user lookup first
573
+ if function_call.name == 'register_new_customer':
574
+ email = args.get('email').lower()
575
+ if mongo.db.users.find_one({'email': email}):
576
+ final_response_text = f"An account with the email '{email}' already exists."
577
+ else:
578
+ hashed_password = bcrypt.generate_password_hash(args.get('password')).decode('utf-8')
579
+ user_doc = { "businessName": args.get('businessName'), "companyName": args.get('businessName'), "email": email, "password": hashed_password, "phoneNumber": args.get('phoneNumber'), "businessAddress": args.get('businessAddress'), "contactPerson": args.get('contactPerson'), "is_approved": False, "is_admin": False, "created_at": datetime.utcnow() }
580
+ mongo.db.users.insert_one(user_doc)
581
+ final_response_text = f"The User , {args.get('contactPerson')} is now registered and their application is pending for approval. After Apprvoal , {args.get('contactPerson')} can log in at https://matax-express.vercel.app/."
582
+
583
+ elif function_call.name == 'get_order_details':
584
+ order_doc = mongo.db.orders.find_one({'_id': ObjectId(args.get("order_id"))}) if ObjectId.is_valid(args.get("order_id")) else mongo.db.orders.find_one({'_id': {'$regex': f'.*{args.get("order_id")}$'}} )
585
+ if not order_doc:
586
+ final_response_text = f"Sorry, I couldn't find an order with ID matching '{args.get('order_id')}'."
587
+ else:
588
+ p_ids = [ObjectId(item['productId']) for item in order_doc.get('items', [])]
589
+ p_map = {str(p['_id']): p for p in mongo.db.products.find({'_id': {'$in': p_ids}})}
590
+ item_lines = [f"- {i['quantity']} {i.get('mode', 'pcs')} of {p_map.get(i['productId'], {}).get('name', 'Unknown')}" for i in order_doc.get('items', [])]
591
+ details = f"*Details for Order #{order['serial_no']}*\n*Customer:* {order_doc.get('user_email')}\n*Status:* {order_doc.get('status', 'N/A')}\n*Delivery:* {order_doc.get('delivery_date', 'N/A')}\n\n*Items:*\n" + "\n".join(item_lines)
592
+ final_response_text = details
593
+
594
+ # For all other functions, perform user lookup
595
+ else:
596
+ user, error_msg = _find_user_by_identifier(args.get("user_identifier"))
597
+ if error_msg:
598
+ final_response_text = error_msg
599
+ else: # User was found successfully
600
+ user_email = user['email']
601
+ user_name = user.get('contactPerson', 'the customer')
602
+
603
+ if function_call.name == 'create_direct_order':
604
+ items_from_args = args.get('items', [])
605
+ validated_items, error_items = [], []
606
+ for item in items_from_args:
607
+ p_doc = mongo.db.products.find_one({'name': {'$regex': f'^{item.get("product_name")}$', '$options': 'i'}})
608
+ if p_doc:
609
+ validated_items.append({"productId": str(p_doc['_id']), "quantity": item.get('quantity'), "mode": item.get('unit')})
610
+ else:
611
+ error_items.append(item.get("product_name"))
612
+
613
+ if error_items:
614
+ final_response_text = f"I couldn't place the order for *{user_name}* because these products were not found: {', '.join(error_items)}. Please try again."
615
+ else:
616
+ next_serial = get_next_order_serial()
617
+ order_doc = {'user_email': user_email, 'items': validated_items, 'delivery_date': args.get('delivery_date'), 'delivery_address': user.get('businessAddress'), 'mobile_number': user.get('phoneNumber'), 'additional_info': args.get('additional_info', ''), 'status': 'pending', 'created_at': datetime.utcnow(),'serial_no': next_serial}
618
+ order_id = mongo.db.orders.insert_one(order_doc).inserted_id
619
+ order_details_for_xero = {
620
+ "order_id": str(order_id), "user_email": user_email, "items": validated_items,
621
+ "delivery_address": user.get('businessAddress'), "mobile_number": user.get('phoneNumber'),"deliverydate": args.get('delivery_date')
622
+ }
623
+ trigger_po_creation(order_details_for_xero)
624
+ final_response_text = f"Thank you! I have directly placed order #{next_serial} for *{user_name}* for delivery on {args.get('delivery_date')}. They can view details at https://matax-express.vercel.app/."
625
+
626
+ elif function_call.name == 'add_items_to_cart':
627
+ items_to_add, added_messages, db_items = args.get('items', []), [], []
628
+ for item in items_to_add:
629
+ p_doc = mongo.db.products.find_one({'name': {'$regex': f'^{item.get("product_name")}$', '$options': 'i'}})
630
+ if p_doc:
631
+ db_items.append({"productId": str(p_doc['_id']), "quantity": item.get('quantity'), "mode": item.get('unit')})
632
+ added_messages.append(f"{item.get('quantity')} {item.get('unit')} of {p_doc['name']}")
633
+ else: added_messages.append(f"could not find '{item.get('product_name')}'")
634
+ if db_items: mongo.db.carts.update_one({'user_email': user_email}, {'$push': {'items': {'$each': db_items}}, '$set': {'updated_at': datetime.utcnow()}}, upsert=True)
635
+ final_response_text = f"OK, I've updated the cart for *{user_name}*: I added {', '.join(added_messages)}."
636
+
637
+ elif function_call.name == 'place_order':
638
+
639
+ next_serial = get_next_order_serial()
640
+ cart = mongo.db.carts.find_one({'user_email': user_email})
641
+ if not cart or not cart.get('items'):
642
+ final_response_text = f"The cart for *{user_name}* is empty. Please add items first."
643
+ else:
644
+ order_doc = {'user_email': user_email, 'items': cart['items'], 'delivery_date': args.get('delivery_date'), 'delivery_address': user.get('businessAddress'), 'mobile_number': user.get('phoneNumber'), 'additional_info': args.get('additional_info', ''), 'status': 'pending', 'created_at': datetime.utcnow(),'serial_no': next_serial}
645
+ order_id = mongo.db.orders.insert_one(order_doc).inserted_id
646
+ order_details_for_xero = {
647
+ "order_id": str(order_id), "user_email": user_email, "items": cart['items'],
648
+ "delivery_address": user.get('businessAddress'), "mobile_number": user.get('phoneNumber'),"deliverydate": args.get('delivery_date')
649
+ }
650
+ trigger_po_creation(order_details_for_xero)
651
+ mongo.db.carts.delete_one({'user_email': user_email}) # Clear cart
652
+ final_response_text = f"Thank you! Order `#{next_serial}` has been placed for *{user_name}* for delivery on {args.get('delivery_date')}. They can view details at https://matax-express.vercel.app/."
653
+
654
+ elif function_call.name == 'get_cart_items':
655
+ cart = mongo.db.carts.find_one({'user_email': user_email})
656
+ if not cart or not cart.get('items'):
657
+ final_response_text = f"The shopping cart for *{user_name}* is currently empty."
658
+ else:
659
+ p_ids = [ObjectId(item['productId']) for item in cart.get('items', [])]
660
+ p_map = {str(p['_id']): p for p in mongo.db.products.find({'_id': {'$in': p_ids}})}
661
+ item_lines = [f"- {i.get('quantity')} {i.get('mode', 'pcs')} of {p_map.get(i.get('productId'), {}).get('name', 'Unknown')}" for i in cart.get('items', [])]
662
+ final_response_text = f"*Here are the items in {user_name}'s cart:*\n" + "\n".join(item_lines) if item_lines else f"The cart for *{user_name}* is empty."
663
+
664
+ elif function_call.name == 'get_my_orders':
665
+ orders = list(mongo.db.orders.find({'user_email': user_email}).sort('created_at', -1).limit(3))
666
+ details_text = ""
667
+ if not orders:
668
+ final_response_text = f"*{user_name}* doesn't have any recent orders."
669
+ else:
670
+ for order in orders:
671
+ order_display_id = order.get('serial_no')
672
+ product_ids = [ObjectId(item['productId']) for item in order.get('items', [])]
673
+ products_map = {str(p['_id']): p for p in mongo.db.products.find({'_id': {'$in': product_ids}})}
674
+ 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', [])]
675
+ details_text += f"\n\n **Details for Order #{order_display_id}** _(Delivery Status: {order.get('status')})_:\n" + "\n".join(item_lines)
676
+ final_response_text ="Here are your recent orders:\n" + details_text
677
+ else:
678
+ final_response_text = response.text
679
+
680
+ # --- 6. Save Conversation to Database ---
681
+ user_message_to_save = user_message_text if user_message_text else "Audio message"
682
+ chat_history.append({"role": "user", "parts": [{"text": user_message_to_save}]})
683
+ chat_history.append({"role": "model", "parts": [{"text": final_response_text}]})
684
+ if len(chat_history) > 10: chat_history = chat_history[-10:]
685
+ mongo.db.whatsapp_history.update_one(
686
+ {'whatsapp_number': whatsapp_number},
687
+ {'$set': {'history': chat_history, 'updated_at': datetime.utcnow()}},
688
+ upsert=True
689
+ )
690
+
691
+ except Exception as e:
692
+ print(traceback.format_exc())
693
+ current_app.logger.error(f"WhatsApp endpoint error: {e}")
694
+ final_response_text = "I'm having a little trouble right now. Please try again in a moment."
695
+
696
+ twilio_resp.message(final_response_text)
697
+ return str(twilio_resp)
app/api.py ADDED
@@ -0,0 +1,392 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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,sync_user_approval_from_xero
9
+ from .email_utils import send_order_confirmation_email, send_registration_email, send_login_notification_email, send_cart_reminder_email
10
+ from .general_utils import get_next_order_serial
11
+
12
+
13
+ api_bp = Blueprint('api', __name__)
14
+ @api_bp.route('/sync_xero_users', methods=['GET'])
15
+ def sync_xero_users():
16
+ sync_user_approval_from_xero()
17
+ return "✅"
18
+
19
+ @api_bp.route('/clear')
20
+ def clear_all():
21
+ mongo.db.users.delete_many({})
22
+ mongo.db.orders.delete_many({})
23
+ return "✅"
24
+
25
+ @api_bp.route('/register', methods=['POST'])
26
+ def register():
27
+ data = request.get_json()
28
+ email = data.get('email')
29
+ password = data.get('password')
30
+ company_name = data.get('businessName')
31
+
32
+ if not all([email, password, company_name]):
33
+ return jsonify({"msg": "Missing required fields: Email, Password, and Business Name"}), 400
34
+ if mongo.db.users.find_one({'email': email}):
35
+ return jsonify({"msg": "A user with this email already exists"}), 409
36
+
37
+ hashed_password = bcrypt.generate_password_hash(password).decode('utf-8')
38
+
39
+ user_document = data.copy()
40
+ user_document['password'] = hashed_password
41
+ user_document['company_name'] = company_name
42
+ user_document['is_approved'] = True
43
+ user_document['is_admin'] = False
44
+
45
+ mongo.db.users.insert_one(user_document)
46
+ trigger_contact_creation(data)
47
+
48
+ try:
49
+ send_registration_email(data)
50
+ except Exception as e:
51
+ current_app.logger.error(f"Failed to send registration email to {email}: {e}")
52
+
53
+ return jsonify({"msg": "Registration successful! Your application is being processed."}), 201
54
+
55
+ # ... (the rest of your api.py file remains unchanged)
56
+ @api_bp.route('/login', methods=['POST'])
57
+ def login():
58
+ data = request.get_json()
59
+ email, password = data.get('email'), data.get('password')
60
+ user = mongo.db.users.find_one({'email': email})
61
+
62
+ if user and user.get('password') and bcrypt.check_password_hash(user['password'], password):
63
+ if not user.get('is_approved', False): return jsonify({"msg": "Account pending approval"}), 403
64
+
65
+ try:
66
+ send_login_notification_email(user)
67
+ except Exception as e:
68
+ current_app.logger.error(f"Failed to send login notification email to {email}: {e}")
69
+
70
+ access_token = create_access_token(identity=email)
71
+ return jsonify(access_token=access_token, email=user['email'], companyName=user['businessName'],contactPerson=user.get('contactPerson', '')) , 200
72
+
73
+ return jsonify({"msg": "Bad email or password"}), 401
74
+
75
+ @api_bp.route('/profile', methods=['GET'])
76
+ @jwt_required()
77
+ def get_user_profile():
78
+ user_email = get_jwt_identity()
79
+ user = mongo.db.users.find_one({'email': user_email})
80
+
81
+ if not user:
82
+ return jsonify({"msg": "User not found"}), 404
83
+
84
+ profile_data = {
85
+ 'deliveryAddress': user.get('businessAddress', ''),
86
+ 'mobileNumber': user.get('phoneNumber', '')
87
+ }
88
+
89
+ return jsonify(profile_data), 200
90
+
91
+ @api_bp.route('/products', methods=['GET'])
92
+ def get_products():
93
+ products = [{
94
+ 'id': str(p['_id']), 'name': p.get('name'), 'category': p.get('category'),
95
+ 'unit': p.get('unit'), 'image_url': p.get('image_url', ''), 'price': p.get('price', '')
96
+ } for p in mongo.db.products.find()]
97
+ return jsonify(products)
98
+
99
+
100
+ @api_bp.route('/cart', methods=['GET', 'POST'])
101
+ @jwt_required()
102
+ def handle_cart():
103
+ user_email = get_jwt_identity()
104
+
105
+ if request.method == 'GET':
106
+ cart = mongo.db.carts.find_one({'user_email': user_email})
107
+ if not cart:
108
+ return jsonify({'items': [], 'deliveryDate': None})
109
+
110
+ populated_items = []
111
+ if cart.get('items'):
112
+ product_ids = [ObjectId(item['productId']) for item in cart['items']]
113
+ if product_ids:
114
+ products = {str(p['_id']): p for p in mongo.db.products.find({'_id': {'$in': product_ids}})}
115
+ for item in cart['items']:
116
+ details = products.get(item['productId'])
117
+ if details:
118
+ populated_items.append({
119
+ 'product': {'id': str(details['_id']), 'name': details.get('name'), 'unit': details.get('unit'), 'image_url': details.get('image_url'), 'price': details.get('price')},
120
+ 'quantity': item['quantity'],
121
+ 'mode': item.get('mode', 'pieces')
122
+ })
123
+
124
+ return jsonify({
125
+ 'items': populated_items,
126
+ 'deliveryDate': cart.get('deliveryDate')
127
+ })
128
+
129
+ if request.method == 'POST':
130
+ data = request.get_json()
131
+
132
+ update_doc = {
133
+ 'user_email': user_email,
134
+ 'updated_at': datetime.utcnow()
135
+ }
136
+
137
+ if 'items' in data:
138
+ update_doc['items'] = data['items']
139
+
140
+ if 'deliveryDate' in data:
141
+ update_doc['deliveryDate'] = data['deliveryDate']
142
+
143
+ mongo.db.carts.update_one(
144
+ {'user_email': user_email},
145
+ {'$set': update_doc},
146
+ upsert=True
147
+ )
148
+ return jsonify({"msg": "Cart updated successfully"})
149
+
150
+ @api_bp.route('/orders', methods=['GET', 'POST'])
151
+ @jwt_required()
152
+ def handle_orders():
153
+ user_email = get_jwt_identity()
154
+
155
+ if request.method == 'POST':
156
+ cart = mongo.db.carts.find_one({'user_email': user_email})
157
+ if not cart or not cart.get('items'): return jsonify({"msg": "Your cart is empty"}), 400
158
+
159
+ data = request.get_json()
160
+ if not all([data.get('deliveryDate'), data.get('deliveryAddress'), data.get('mobileNumber')]): return jsonify({"msg": "Missing delivery information"}), 400
161
+
162
+ user = mongo.db.users.find_one({'email': user_email})
163
+ if not user:
164
+ return jsonify({"msg": "User not found"}), 404
165
+
166
+ order_doc = {
167
+ 'user_email': user_email, 'items': cart['items'], 'delivery_date': data['deliveryDate'],
168
+ 'delivery_address': data['deliveryAddress'], 'mobile_number': data['mobileNumber'],
169
+ 'additional_info': data.get('additionalInfo'), 'total_amount': data.get('totalAmount'),
170
+ 'status': 'pending', 'created_at': datetime.utcnow()
171
+ }
172
+ order_doc['serial_no'] = get_next_order_serial()
173
+ order_id = mongo.db.orders.insert_one(order_doc).inserted_id
174
+ order_doc['_id'] = order_id
175
+
176
+ order_details_for_xero = {
177
+ "order_id": str(order_id), "user_email": user_email, "items": cart['items'],
178
+ "delivery_address": data['deliveryAddress'], "mobile_number": data['mobileNumber'],"deliverydate":data["deliveryDate"]
179
+ }
180
+ trigger_po_creation(order_details_for_xero)
181
+
182
+ try:
183
+ product_ids = [ObjectId(item['productId']) for item in cart['items']]
184
+ products_map = {str(p['_id']): p for p in mongo.db.products.find({'_id': {'$in': product_ids}})}
185
+
186
+ order_doc['populated_items'] = [{
187
+ "name": products_map.get(item['productId'], {}).get('name', 'N/A'),
188
+ "quantity": item['quantity'],
189
+ "mode": item.get('mode', 'pieces')
190
+ } for item in cart['items']]
191
+
192
+ send_order_confirmation_email(order_doc, user)
193
+
194
+ except Exception as e:
195
+ current_app.logger.error(f"Failed to send confirmation email for order {order_id}: {e}")
196
+
197
+ mongo.db.carts.delete_one({'user_email': user_email})
198
+ return jsonify({"msg": "Order placed successfully! You will be redirected shortly to the Orders Page!", "orderId": str(order_id)}), 201
199
+
200
+ if request.method == 'GET':
201
+ user_orders = list(mongo.db.orders.find({'user_email': user_email}).sort('created_at', -1))
202
+ if not user_orders: return jsonify([])
203
+
204
+ all_product_ids = {ObjectId(item['productId']) for order in user_orders for item in order.get('items', [])}
205
+ products = {str(p['_id']): p for p in mongo.db.products.find({'_id': {'$in': list(all_product_ids)}})}
206
+
207
+ for order in user_orders:
208
+ order['items'] = [
209
+ {
210
+ 'quantity': item['quantity'],
211
+ 'mode': item.get('mode', 'pieces'),
212
+ 'product': {
213
+ 'id': str(p['_id']),
214
+ 'name': p.get('name'),
215
+ 'unit': p.get('unit'),
216
+ 'image_url': p.get('image_url')
217
+ }
218
+ }
219
+ for item in order.get('items', []) if (p := products.get(item['productId']))
220
+ ]
221
+ order['_id'] = str(order['_id'])
222
+ order['created_at'] = order['created_at'].isoformat()
223
+ order['delivery_date'] = order['delivery_date'] if isinstance(order['delivery_date'], str) else order['delivery_date'].isoformat()
224
+ return jsonify(user_orders)
225
+
226
+ @api_bp.route('/orders/<order_id>', methods=['GET'])
227
+ @jwt_required()
228
+ def get_order(order_id):
229
+ user_email = get_jwt_identity()
230
+ try:
231
+ order = mongo.db.orders.find_one({'_id': ObjectId(order_id), 'user_email': user_email})
232
+ if not order:
233
+ return jsonify({"msg": "Order not found or access denied"}), 404
234
+
235
+ order['_id'] = str(order['_id'])
236
+ return jsonify(order), 200
237
+ except Exception as e:
238
+ return jsonify({"msg": f"Invalid Order ID format: {e}"}), 400
239
+
240
+ @api_bp.route('/orders/<order_id>', methods=['PUT'])
241
+ @jwt_required()
242
+ def update_order(order_id):
243
+ user_email = get_jwt_identity()
244
+
245
+ order = mongo.db.orders.find_one({'_id': ObjectId(order_id), 'user_email': user_email})
246
+ if not order:
247
+ return jsonify({"msg": "Order not found or access denied"}), 404
248
+
249
+ if order.get('status') not in ['pending', 'confirmed']:
250
+ return jsonify({"msg": f"Order with status '{order.get('status')}' cannot be modified."}), 400
251
+
252
+ cart = mongo.db.carts.find_one({'user_email': user_email})
253
+ if not cart or not cart.get('items'):
254
+ return jsonify({"msg": "Cannot update with an empty cart. Please add items."}), 400
255
+
256
+ data = request.get_json()
257
+ update_doc = {
258
+ 'items': cart['items'],
259
+ 'delivery_date': data['deliveryDate'],
260
+ 'delivery_address': data['deliveryAddress'],
261
+ 'mobile_number': data['mobileNumber'],
262
+ 'additional_info': data.get('additionalInfo'),
263
+ 'total_amount': data.get('totalAmount'),
264
+ 'updated_at': datetime.utcnow()
265
+ }
266
+
267
+ mongo.db.orders.update_one({'_id': ObjectId(order_id)}, {'$set': update_doc})
268
+ mongo.db.carts.delete_one({'user_email': user_email})
269
+
270
+ return jsonify({"msg": "Order updated successfully!", "orderId": order_id}), 200
271
+
272
+ @api_bp.route('/orders/<order_id>/cancel', methods=['POST'])
273
+ @jwt_required()
274
+ def cancel_order(order_id):
275
+ user_email = get_jwt_identity()
276
+ order = mongo.db.orders.find_one({'_id': ObjectId(order_id), 'user_email': user_email})
277
+
278
+ if not order:
279
+ return jsonify({"msg": "Order not found or access denied"}), 404
280
+
281
+ if order.get('status') in ['delivered', 'cancelled']:
282
+ return jsonify({"msg": "This order can no longer be cancelled."}), 400
283
+
284
+ mongo.db.orders.update_one(
285
+ {'_id': ObjectId(order_id)},
286
+ {'$set': {'status': 'cancelled', 'updated_at': datetime.utcnow()}}
287
+ )
288
+
289
+ return jsonify({"msg": "Order has been cancelled."}), 200
290
+
291
+ @api_bp.route('/sendmail', methods=['GET'])
292
+ def send_cart_reminders():
293
+ try:
294
+ carts_with_items = list(mongo.db.carts.find({'items': {'$exists': True, '$ne': []}}))
295
+
296
+ if not carts_with_items:
297
+ return jsonify({"msg": "No users with pending items in cart."}), 200
298
+
299
+ user_emails = [cart['user_email'] for cart in carts_with_items]
300
+ all_product_ids = {
301
+ ObjectId(item['productId'])
302
+ for cart in carts_with_items
303
+ for item in cart.get('items', [])
304
+ }
305
+
306
+ users_cursor = mongo.db.users.find({'email': {'$in': user_emails}})
307
+ products_cursor = mongo.db.products.find({'_id': {'$in': list(all_product_ids)}})
308
+
309
+ users_map = {user['email']: user for user in users_cursor}
310
+ products_map = {str(prod['_id']): prod for prod in products_cursor}
311
+
312
+ emails_sent_count = 0
313
+
314
+ for cart in carts_with_items:
315
+ user = users_map.get(cart['user_email'])
316
+ if not user:
317
+ current_app.logger.warning(f"Cart found for non-existent user: {cart['user_email']}")
318
+ continue
319
+
320
+ populated_items = []
321
+ for item in cart.get('items', []):
322
+ product_details = products_map.get(item['productId'])
323
+ if product_details:
324
+ populated_items.append({
325
+ 'product': {
326
+ 'id': str(product_details['_id']),
327
+ 'name': product_details.get('name'),
328
+ },
329
+ 'quantity': item['quantity']
330
+ })
331
+
332
+ if populated_items:
333
+ try:
334
+ send_cart_reminder_email(user, populated_items)
335
+ emails_sent_count += 1
336
+ except Exception as e:
337
+ current_app.logger.error(f"Failed to send cart reminder to {user['email']}: {e}")
338
+
339
+ return jsonify({"msg": f"Cart reminder process finished. Emails sent to {emails_sent_count} users."}), 200
340
+
341
+ except Exception as e:
342
+ current_app.logger.error(f"Error in /sendmail endpoint: {e}")
343
+ return jsonify({"msg": "An internal error occurred while sending reminders."}), 500
344
+
345
+ @api_bp.route('/admin/users/approve/<user_id>', methods=['POST'])
346
+ @jwt_required()
347
+ def approve_user(user_id):
348
+ mongo.db.users.update_one({'_id': ObjectId(user_id)}, {'$set': {'is_approved': True}})
349
+ return jsonify({"msg": f"User {user_id} approved"})
350
+
351
+ # +++ START: NEW ENDPOINT FOR ITEM REQUESTS +++
352
+ @api_bp.route('/request-item', methods=['POST'])
353
+ @jwt_required()
354
+ def request_item():
355
+ """
356
+ Allows a logged-in user to request an item that is not in the catalog.
357
+ The request is saved to the database for admin review.
358
+ """
359
+ user_email = get_jwt_identity()
360
+ data = request.get_json()
361
+
362
+ if not data or not data.get('details'):
363
+ return jsonify({"msg": "Item details are required."}), 400
364
+
365
+ details = data.get('details').strip()
366
+ if not details:
367
+ return jsonify({"msg": "Item details cannot be empty."}), 400
368
+
369
+ try:
370
+ # Fetch user info for more context in the request
371
+ user = mongo.db.users.find_one({'email': user_email}, {'company_name': 1})
372
+ company_name = user.get('company_name', 'N/A') if user else 'N/A'
373
+
374
+ request_doc = {
375
+ 'user_email': user_email,
376
+ 'company_name': company_name,
377
+ 'details': details,
378
+ 'status': 'new', # Possible statuses: 'new', 'reviewed', 'sourced', 'rejected'
379
+ 'requested_at': datetime.utcnow()
380
+ }
381
+
382
+ # The collection 'item_requests' will be created if it doesn't exist
383
+ mongo.db.item_requests.insert_one(request_doc)
384
+
385
+ # Optional: Here you could add a call to an email utility to notify admins
386
+ # For example: send_item_request_notification(user_email, company_name, details)
387
+
388
+ return jsonify({"msg": "Your item request has been submitted. We will look into it!"}), 201
389
+
390
+ except Exception as e:
391
+ current_app.logger.error(f"Error processing item request for {user_email}: {e}")
392
+ return jsonify({"msg": "An internal server error occurred."}), 500
app/config.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # from dotenv import load_dotenv
2
+ # load_dotenv(r'C:\Users\Vaibhav Arora\Documents\MyExperimentsandCodes\APPS_WEBSITES\CANADA_WHOLESALE_PROJECT\GITHUB_REPOS\mvp-vue\wholesale-grocery-app\AIAPPS\.env')
3
+ import os
4
+ from datetime import timedelta
5
+
6
+ # It is recommended to load sensitive data from environment variables
7
+ SECRET_KEY = os.environ.get("SECRET_KEY")
8
+ SESSION_TYPE = 'filesystem'
9
+ DEBUG = True
10
+
11
+ # MongoDB
12
+ MONGO_URI = os.environ.get(
13
+ "MONGO_URI")
14
+ # JWT
15
+ JWT_SECRET_KEY = os.environ.get("JWT_SECRET_KEY")
16
+ JWT_ACCESS_TOKEN_EXPIRES = timedelta(days=7)
17
+
18
+ # Xero OAuth Credentials
19
+ CLIENT_ID = os.environ.get("CLIENT_ID")
20
+ CLIENT_SECRET = os.environ.get("CLIENT_SECRET")
21
+ BREVO_API_KEY=os.environ.get("BREVO_API_KEY")
22
+ CLIENT_ADMIN_EMAIL="namita0105@gmail.com"
23
+ SENDER_EMAIL="vaibhavarduino@gmail.com"
app/email_utils.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # your_app/email_utils.py
2
+
3
+ from flask import current_app
4
+ import sib_api_v3_sdk
5
+ from sib_api_v3_sdk.rest import ApiException
6
+ from .general_utils import backfill_orders
7
+
8
+
9
+ def send_order_confirmation_email(order, user):
10
+ """
11
+ Constructs and sends order confirmation emails to the customer and a notification
12
+ to the client/admin using the Brevo API.
13
+
14
+ Args:
15
+ order (dict): The complete order document, including 'populated_items'.
16
+ user (dict): The user document, containing 'email' and 'company_name'.
17
+ """
18
+ # Setup Brevo API client from Flask app config
19
+ configuration = sib_api_v3_sdk.Configuration()
20
+ configuration.api_key['api-key'] = current_app.config['BREVO_API_KEY']
21
+ api_instance = sib_api_v3_sdk.TransactionalEmailsApi(sib_api_v3_sdk.ApiClient(configuration))
22
+
23
+ # --- Common variables for emails ---
24
+ sender_email = current_app.config['SENDER_EMAIL']
25
+ sender_name = "Matax Express" # You can customize this
26
+ client_admin_email = current_app.config['CLIENT_ADMIN_EMAIL']
27
+ customer_email = user['email']
28
+ contact_person = user['contactPerson']
29
+ order_id_str = str(order['serial_no'])
30
+
31
+ # --- Generate HTML table of ordered items ---
32
+ items_html = "<table border='1' cellpadding='5' cellspacing='0' style='border-collapse: collapse; width: 100%;'><tr><th style='text-align: left;'>Product</th><th style='text-align: center;'>Quantity</th></tr>"
33
+ for item in order['populated_items']:
34
+ if item['mode'] == 'weight':
35
+ item['mode']='lb'
36
+ items_html += f"<tr><td>{item['name']}</td><td style='text-align: center;'>{item['quantity']} {item['mode']}</td></tr>"
37
+ items_html += "</table>"
38
+
39
+ # --- 1. Prepare Email for the Customer ---
40
+ customer_subject = f"Your Order Confirmation - #{order_id_str}"
41
+ customer_html_content = f"""
42
+ <html><body>
43
+ <h1>Thank You for Your Order, {contact_person}!</h1>
44
+ <p>We have successfully received your order #{order_id_str}. Here are the details:</p>
45
+ <h2>Order Summary</h2>
46
+ {items_html}
47
+ <h2>Delivery Details</h2>
48
+ <p><strong>Delivery Date:</strong> {order['delivery_date']}</p>
49
+ <p><strong>Delivery Address:</strong> {order['delivery_address']}</p>
50
+ <p><strong>Contact Number:</strong> {order['mobile_number']}</p>
51
+ <p><strong>Additional Info:</strong> {order.get('additional_info') or 'N/A'}</p>
52
+ <p>Thank you for your business!</p>
53
+ </body></html>
54
+ """
55
+
56
+ send_smtp_email_customer = sib_api_v3_sdk.SendSmtpEmail(
57
+ to=[{"email": customer_email, "name": contact_person}],
58
+ sender={"email": sender_email, "name": sender_name},
59
+ subject=customer_subject,
60
+ html_content=customer_html_content
61
+ )
62
+
63
+ # --- 2. Prepare Email for the Client/Admin ---
64
+ client_subject = f"New Order Received - #{order_id_str} from {contact_person}"
65
+ client_html_content = f"""
66
+ <html><body>
67
+ <h1>New Order Received!</h1>
68
+ <p>A new order #{order_id_str} has been placed by <strong>{contact_person}</strong>.</p>
69
+ <h2>Order Details</h2>
70
+ {items_html}
71
+ <h2>Customer & Delivery Information</h2>
72
+ <p><strong>Company:</strong> {contact_person}</p>
73
+ <p><strong>Email:</strong> {customer_email}</p>
74
+ <p><strong>Delivery Date:</strong> {order['delivery_date']}</p>
75
+ <p><strong>Delivery Address:</strong> {order['delivery_address']}</p>
76
+ <p><strong>Contact Number:</strong> {order['mobile_number']}</p>
77
+ <p><strong>Additional Info:</strong> {order.get('additional_info') or 'N/A'}</p>
78
+ <p><strong>Total Amount:</strong> ${order.get('total_amount', 0):.2f}</p>
79
+ <p>This order has also been sent for PO creation in Xero.</p>
80
+ </body></html>
81
+ """
82
+
83
+ send_smtp_email_client = sib_api_v3_sdk.SendSmtpEmail(
84
+ to=[{"email": client_admin_email}],
85
+ sender={"email": sender_email, "name": sender_name},
86
+ subject=client_subject,
87
+ html_content=client_html_content
88
+ )
89
+
90
+ # --- 3. Send both emails ---
91
+ api_instance.send_transac_email(send_smtp_email_customer)
92
+ current_app.logger.info(f"Order confirmation sent to {customer_email} for order {order_id_str}")
93
+
94
+ api_instance.send_transac_email(send_smtp_email_client)
95
+ current_app.logger.info(f"New order notification sent to {client_admin_email} for order {order_id_str}")
96
+
97
+ # +++ START: NEW EMAIL FUNCTIONS +++
98
+
99
+ def send_registration_email(user_data):
100
+ """Sends a welcome/application pending email upon registration."""
101
+ configuration = sib_api_v3_sdk.Configuration()
102
+ configuration.api_key['api-key'] = current_app.config['BREVO_API_KEY']
103
+ api_instance = sib_api_v3_sdk.TransactionalEmailsApi(sib_api_v3_sdk.ApiClient(configuration))
104
+
105
+ sender_email = current_app.config['SENDER_EMAIL']
106
+ sender_name = "Matax Express"
107
+ recipient_email = user_data['email']
108
+ recipient_name = user_data.get('contactPerson', user_data.get('businessName', 'New User'))
109
+
110
+ subject = "your application is under process"
111
+ html_content = f"""
112
+ <html><body>
113
+ <h1>Welcome to Matax Express, {recipient_name}!</h1>
114
+ <p>Thank you for registering. Your application is currently under review by our team.</p>
115
+ <p>We will notify you via email as soon as your account is approved.</p>
116
+ <p>Thank you for your patience.</p>
117
+ <p>Best regards,<br>The Matax Express Team</p>
118
+ </body></html>
119
+ """
120
+
121
+ send_smtp_email = sib_api_v3_sdk.SendSmtpEmail(
122
+ to=[{ "email": recipient_email, "name": recipient_name }],
123
+ sender={ "email": sender_email, "name": sender_name },
124
+ subject=subject,
125
+ html_content=html_content
126
+ )
127
+
128
+ try:
129
+ api_instance.send_transac_email(send_smtp_email)
130
+ current_app.logger.info(f"Registration email sent to {recipient_email}")
131
+ except ApiException as e:
132
+ current_app.logger.error(f"Failed to send registration email to {recipient_email}: {e}")
133
+
134
+ def send_login_notification_email(user):
135
+ """Sends a notification email upon successful login."""
136
+ configuration = sib_api_v3_sdk.Configuration()
137
+ configuration.api_key['api-key'] = current_app.config['BREVO_API_KEY']
138
+ api_instance = sib_api_v3_sdk.TransactionalEmailsApi(sib_api_v3_sdk.ApiClient(configuration))
139
+
140
+ sender_email = current_app.config['SENDER_EMAIL']
141
+ sender_name = "Matax Express"
142
+ recipient_email = user['email']
143
+ recipient_name = user.get('contactPerson', user.get('company_name', 'Valued Customer'))
144
+
145
+ subject = "New login.."
146
+ html_content = f"""
147
+ <html><body>
148
+ <h1>Security Alert: New Login</h1>
149
+ <p>Hello {recipient_name},</p>
150
+ <p>We detected a new login to your Matax Express account just now.</p>
151
+ <p>If this was you, you can safely disregard this email. If you do not recognize this activity, please change your password immediately and contact our support team.</p>
152
+ <p>Thank you for helping us keep your account secure.</p>
153
+ <p>Best regards,<br>The Matax Express Team</p>
154
+ </body></html>
155
+ """
156
+
157
+ send_smtp_email = sib_api_v3_sdk.SendSmtpEmail(
158
+ to=[{ "email": recipient_email, "name": recipient_name }],
159
+ sender={ "email": sender_email, "name": sender_name },
160
+ subject=subject,
161
+ html_content=html_content
162
+ )
163
+
164
+ try:
165
+ api_instance.send_transac_email(send_smtp_email)
166
+ current_app.logger.info(f"Login notification sent to {recipient_email}")
167
+ except ApiException as e:
168
+ current_app.logger.error(f"Failed to send login notification to {recipient_email}: {e}")
169
+
170
+ def send_cart_reminder_email(user, populated_items):
171
+ """Sends a reminder for items left in the cart."""
172
+ configuration = sib_api_v3_sdk.Configuration()
173
+ configuration.api_key['api-key'] = current_app.config['BREVO_API_KEY']
174
+ api_instance = sib_api_v3_sdk.TransactionalEmailsApi(sib_api_v3_sdk.ApiClient(configuration))
175
+
176
+ sender_email = current_app.config['SENDER_EMAIL']
177
+ sender_name = "Matax Express"
178
+ recipient_email = user['email']
179
+ recipient_name = user.get('contactPerson', user.get('company_name', 'Valued Customer'))
180
+
181
+ subject = "Your items are waiting for you!"
182
+
183
+ items_html = "<table border='1' cellpadding='5' cellspacing='0' style='border-collapse: collapse; width: 100%;'><tr><th style='text-align: left;'>Product</th><th style='text-align: center;'>Quantity</th></tr>"
184
+ for item in populated_items:
185
+ items_html += f"<tr><td>{item['product']['name']}</td><td style='text-align: center;'>{item['quantity']}</td></tr>"
186
+ items_html += "</table>"
187
+
188
+ html_content = f"""
189
+ <html><body>
190
+ <h1>Don't Forget Your Items, {recipient_name}!</h1>
191
+ <p>Your baby fruits are waiting to arrive! You have some delicious items waiting in your cart.</p>
192
+ <br>
193
+ {items_html}
194
+ <br>
195
+ <p>Ready to complete your order? Please log in to your account to view your cart.</p>
196
+ <p>These items are popular and might not be available for long!</p>
197
+ <p>Best regards,<br>The Matax Express Team</p>
198
+ </body></html>
199
+ """
200
+
201
+ send_smtp_email = sib_api_v3_sdk.SendSmtpEmail(
202
+ to=[{ "email": recipient_email, "name": recipient_name }],
203
+ sender={ "email": sender_email, "name": sender_name },
204
+ subject=subject,
205
+ html_content=html_content
206
+ )
207
+
208
+ try:
209
+ api_instance.send_transac_email(send_smtp_email)
210
+ current_app.logger.info(f"Cart reminder sent to {recipient_email}")
211
+ except ApiException as e:
212
+ current_app.logger.error(f"Failed to send cart reminder to {recipient_email}: {e}")
213
+
214
+ # +++ END: NEW EMAIL FUNCTIONS +++
app/extensions.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask_pymongo import PyMongo
2
+ from flask_bcrypt import Bcrypt
3
+ from flask_jwt_extended import JWTManager
4
+ from flask_cors import CORS
5
+ from flask_session import Session
6
+ from flask_oauthlib.contrib.client import OAuth
7
+
8
+ mongo = PyMongo()
9
+ bcrypt = Bcrypt()
10
+ jwt = JWTManager()
11
+ cors = CORS()
12
+ session = Session()
13
+ oauth = OAuth()
app/general_utils.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pymongo import ASCENDING, DESCENDING
2
+ import time
3
+ from .extensions import mongo
4
+ def backfill_orders():
5
+ t=time.time()
6
+ print("Starting backfill process for order serial numbers...")
7
+
8
+ # 1. Find the highest existing serial number to know where to start.
9
+ highest_order = mongo.db.orders.find_one(
10
+ {'serial_no': {'$exists': True}},
11
+ sort=[('serial_no', DESCENDING)]
12
+ )
13
+
14
+ next_serial_to_assign = 1
15
+ if highest_order and 'serial_no' in highest_order:
16
+ next_serial_to_assign = highest_order['serial_no'] + 1
17
+ print(f"Highest existing serial is {highest_order['serial_no']}. New serials will start from {next_serial_to_assign}.")
18
+ else:
19
+ print("No existing serial numbers found. Starting from 1.")
20
+
21
+ # 2. Find all orders that are missing a serial number, sorted by creation date.
22
+ orders_to_update = list(mongo.db.orders.find(
23
+ {'serial_no': {'$exists': False}},
24
+ sort=[('created_at', ASCENDING)]
25
+ ))
26
+
27
+ if not orders_to_update:
28
+ print("No orders found without a serial number. All good!")
29
+ return
30
+
31
+ print(f"Found {len(orders_to_update)} orders to update.")
32
+
33
+ # 3. Iterate and assign the new serial numbers.
34
+ for order in orders_to_update:
35
+ mongo.db.orders.update_one(
36
+ {'_id': order['_id']},
37
+ {'$set': {'serial_no': next_serial_to_assign}}
38
+ )
39
+ print(f" - Updated order {order['_id']} with Serial No: {next_serial_to_assign}")
40
+ next_serial_to_assign += 1
41
+
42
+ # 4. Finally, update the global counter to the last assigned value.
43
+ final_serial_value = next_serial_to_assign - 1
44
+ mongo.db.counters.update_one(
45
+ {'_id': 'order_serial'},
46
+ {'$set': {'sequence_value': final_serial_value}},
47
+ upsert=True
48
+ )
49
+ print(time.time()-t)
50
+ print(f"\nBackfill complete. Updated {len(orders_to_update)} orders.")
51
+ print(f"Global order serial counter has been set to {final_serial_value}.")
52
+
53
+ # --- NEW: Define functions for the AI model ---
54
+ def get_next_order_serial():
55
+ """
56
+ Atomically retrieves and increments the global order serial number from the 'counters' collection.
57
+ """
58
+ # find_one_and_update is atomic. upsert=True creates the doc if it doesn't exist.
59
+ # By default, it returns the document *before* the update.
60
+ counter_doc = mongo.db.counters.find_one_and_update(
61
+ {'_id': 'order_serial'},
62
+ {'$inc': {'sequence_value': 1}},
63
+ upsert=True
64
+ )
65
+ # If counter_doc is None, it means the document was just created (upserted) with a value of 1.
66
+ if counter_doc is None:
67
+ return 1
68
+ # Otherwise, it returns the old document, so we add 1 to get the new value.
69
+ else:
70
+ return counter_doc['sequence_value'] + 1
app/models.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ from .extensions import mongo
2
+
3
+ users_collection = mongo.db.users
4
+ products_collection = mongo.db.products
5
+ orders_collection = mongo.db.orders
6
+ carts_collection = mongo.db.carts
7
+ xero_tokens_collection = mongo.db.xero_tokens
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/templates/base.html ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>{% if title %}{{ title }}{% else %}Welcome to Xero Python oauth starter{% endif %}</title>
6
+ </head>
7
+ <body>
8
+ <a href="{{ url_for('xero.index') }}">Index</a> |
9
+ <a href="{{ url_for('xero.login') }}">Login</a> |
10
+ <a href="{{ url_for('xero.logout') }}">Logout</a> |
11
+
12
+ <a href="{{ url_for('xero.fetch_inventory') }}">Sync Xero Inventory with Website</a> |
13
+ {% block content %}{% endblock %}
14
+ <!--<script src="https://cdn.jsdelivr.net/gh/google/code-prettify@master/loader/run_prettify.js"></script>-->
15
+ </body>
16
+ </html>
app/templates/code.html ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block content %}
4
+ <div>
5
+ <h2>{{ title }}</h2>
6
+ {% if sub_title %}<h3>{{ sub_title }}</h3>{% endif %}
7
+ {% if result_list %}
8
+ <ul>
9
+ {% for message in result_list %}
10
+ <li>{{ message }}</li>
11
+ {% endfor %}
12
+ </ul>
13
+ {% endif %}
14
+ <pre class="prettyprint"><code class="language-javascript">{{ code }}</code></pre>
15
+ </div>
16
+ {% endblock %}
app/xero_client.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from functools import wraps
3
+ from flask import redirect, url_for
4
+ from xero_python.api_client import ApiClient, Configuration
5
+ from xero_python.api_client.oauth2 import OAuth2Token # <-- IMPORT THIS
6
+ from xero_python.identity import IdentityApi
7
+ from .extensions import oauth, mongo
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ # Pass an empty OAuth2Token object during initialization
12
+ api_client = ApiClient(
13
+ Configuration(oauth2_token=OAuth2Token()),
14
+ pool_threads=1
15
+ )
16
+
17
+ xero = oauth.remote_app(
18
+ name="xero",
19
+ version="2",
20
+ client_id=None, # This will be set in create_app
21
+ client_secret=None, # This will be set in create_app
22
+ endpoint_url="https://api.xero.com/",
23
+ authorization_url="https://login.xero.com/identity/connect/authorize",
24
+ access_token_url="https://identity.xero.com/connect/token",
25
+ refresh_token_url="https://identity.xero.com/connect/token",
26
+ scope="offline_access openid profile email accounting.transactions "
27
+ "accounting.journals.read accounting.transactions payroll.payruns accounting.reports.read "
28
+ "files accounting.settings.read accounting.settings accounting.attachments payroll.payslip payroll.settings files.read openid assets.read profile payroll.employees projects.read email accounting.contacts.read accounting.attachments.read projects assets accounting.contacts payroll.timesheets accounting.budgets.read",
29
+ )
30
+ @xero.tokengetter
31
+ @api_client.oauth2_token_getter
32
+ def obtain_xero_oauth2_token():
33
+ token_doc = mongo.db.xero_tokens.find_one({'name': 'xero_app_token'})
34
+ return token_doc.get('token') if token_doc else None
35
+
36
+ @xero.tokensaver
37
+ @api_client.oauth2_token_saver
38
+ def store_xero_oauth2_token(token):
39
+ if token:
40
+ mongo.db.xero_tokens.update_one(
41
+ {'name': 'xero_app_token'},
42
+ {'$set': {'token': token}},
43
+ upsert=True
44
+ )
45
+ logger.info("Xero token stored/updated in database.")
46
+ else:
47
+ if mongo.db.xero_tokens.delete_one({'name': 'xero_app_token'}).deleted_count > 0:
48
+ logger.info("Xero token removed from database.")
49
+
50
+ def get_xero_tenant_id():
51
+ token = obtain_xero_oauth2_token()
52
+ if not token:
53
+ return None
54
+
55
+ identity_api = IdentityApi(api_client)
56
+ for connection in identity_api.get_connections():
57
+ if connection.tenant_type == "ORGANISATION":
58
+ return connection.tenant_id
59
+
60
+ def xero_token_required(function):
61
+ @wraps(function)
62
+ def decorator(*args, **kwargs):
63
+ if not obtain_xero_oauth2_token():
64
+ return redirect(url_for("xero.login", _external=True))
65
+ return function(*args, **kwargs)
66
+ return decorator
app/xero_routes.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import logging
3
+ from flask import Blueprint, render_template, redirect, url_for, jsonify, request
4
+ from pymongo import UpdateOne
5
+ from xero_python.accounting import AccountingApi
6
+ from utils import jsonify as jsonify_xero
7
+ from search_engine import search_and_filter_images, categorise
8
+ from .xero_client import xero, api_client, store_xero_oauth2_token, obtain_xero_oauth2_token, xero_token_required, get_xero_tenant_id
9
+ from .extensions import mongo # <-- IMPORT MONGO
10
+ import traceback
11
+ xero_bp = Blueprint('xero', __name__)
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ @xero_bp.route("/")
16
+ def index():
17
+ xero_access = dict(obtain_xero_oauth2_token() or {})
18
+ return render_template(
19
+ "code.html",
20
+ title="Home | OAuth Token",
21
+ code=json.dumps(xero_access, sort_keys=True, indent=4),
22
+ )
23
+
24
+ @xero_bp.route("/login")
25
+ def login():
26
+ redirect_url = url_for("xero.oauth_callback", _external=True)
27
+ return xero.authorize(callback_uri=redirect_url)
28
+
29
+ @xero_bp.route("/callback")
30
+ def oauth_callback():
31
+ try:
32
+ response = xero.authorized_response()
33
+ except Exception as e:
34
+ logger.error("OAuth callback failed: %s", e)
35
+ return "OAuth callback failed.", 500
36
+
37
+ if response is None or response.get("access_token") is None:
38
+ return "Access denied: response=%s" % response
39
+
40
+ store_xero_oauth2_token(response)
41
+ return redirect(url_for("xero.index", _external=True))
42
+
43
+ @xero_bp.route("/logout")
44
+ def logout():
45
+ store_xero_oauth2_token(None)
46
+ return redirect(url_for("xero.index", _external=True))
47
+
48
+ @xero_bp.route("/api/inventory")
49
+ @xero_token_required
50
+ def fetch_inventory():
51
+ try:
52
+ xero_tenant_id = get_xero_tenant_id()
53
+ accounting_api = AccountingApi(api_client)
54
+
55
+ xero_items = accounting_api.get_items(xero_tenant_id=xero_tenant_id).items
56
+ fetched_products = [{
57
+ "code": item.code, "name": item.name,
58
+ "price": float(item.sales_details.unit_price) if item.sales_details else 0.0,
59
+ "unit": item.description, "on_hand": item.quantity_on_hand if item.quantity_on_hand else 1,
60
+ } for item in xero_items]
61
+
62
+ # Use mongo.db directly
63
+ db_products = list(mongo.db.products.find({}))
64
+ db_products_map = {p['code']: p for p in db_products if 'code' in p}
65
+ fetched_products_map = {p['code']: p for p in fetched_products}
66
+
67
+ products_to_insert = []
68
+ bulk_update_ops = []
69
+
70
+ for code, fetched_p in fetched_products_map.items():
71
+ db_p = db_products_map.get(code)
72
+ if not db_p:
73
+ new_doc = { "code": fetched_p["code"], "name": fetched_p["name"], "price": fetched_p["price"], "unit": fetched_p["unit"],
74
+ "category": str(categorise(fetched_p["name"])),
75
+ "image_url": str(search_and_filter_images(str(fetched_p["name"]))[0]["image_url"]),}
76
+ products_to_insert.append(new_doc)
77
+ elif (fetched_p['name'] != db_p.get('name') or fetched_p['price'] != db_p.get('price') or fetched_p['unit'] != db_p.get('unit')):
78
+ update_fields = { "name": fetched_p["name"], "price": fetched_p["price"], "unit": fetched_p["unit"] }
79
+ if fetched_p['name'] != db_p.get('name'):
80
+ update_fields["category"] = str(categorise(fetched_p["name"]))
81
+ update_fields["image_url"] = str(search_and_filter_images(str(fetched_p["name"]))[0]["image_url"])
82
+ bulk_update_ops.append(UpdateOne({'code': code}, {'$set': update_fields}))
83
+
84
+ db_codes = set(db_products_map.keys())
85
+ fetched_codes = set(fetched_products_map.keys())
86
+ codes_to_delete = list(db_codes - fetched_codes)
87
+
88
+ # Use mongo.db directly
89
+ if codes_to_delete: mongo.db.products.delete_many({'code': {'$in': codes_to_delete}})
90
+ if products_to_insert: mongo.db.products.insert_many(products_to_insert)
91
+ if bulk_update_ops: mongo.db.products.bulk_write(bulk_update_ops)
92
+
93
+ sub_title = (f"Sync complete. Inserted: {len(products_to_insert)}, "
94
+ f"Updated: {len(bulk_update_ops)}, Deleted: {len(codes_to_delete)}")
95
+ code_to_display = jsonify_xero(fetched_products)
96
+
97
+ except Exception as e:
98
+ print(traceback.format_exc())
99
+ logger.error("Inventory sync failed: %s", e)
100
+ sub_title = f"Error during sync: {e}"
101
+ code_to_display = jsonify({"error": str(e)})
102
+
103
+ return render_template("code.html", title="Inventory Sync", sub_title=sub_title, code=code_to_display)
104
+
app/xero_utils.py ADDED
@@ -0,0 +1,346 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import threading
3
+ from datetime import datetime
4
+ from flask import current_app
5
+ from bson.objectid import ObjectId
6
+ from xero_python.accounting import (
7
+ AccountingApi, Contact, Contacts, ContactPerson,ContactGroup,ContactGroups, PurchaseOrder,
8
+ PurchaseOrders, LineItem, Address, Phone, HistoryRecord, HistoryRecords
9
+ )
10
+
11
+ from .xero_client import api_client,get_xero_tenant_id
12
+ from .extensions import mongo # <-- IMPORT MONGO
13
+
14
+ logger = logging.getLogger(__name__)
15
+ def sync_user_approval_from_xero():
16
+ """
17
+ Iterates through all users in MongoDB and updates their 'is_approved' status
18
+ based on their membership in the 'approve_user' contact group in Xero.
19
+ This function is intended to be run periodically (e.g., by a scheduler).
20
+ """
21
+ logger.info("Starting Xero 'approve_user' contact group sync.")
22
+ try:
23
+ xero_tenant_id = get_xero_tenant_id()
24
+ accounting_api = AccountingApi(api_client)
25
+
26
+ # --- Step 1: Find the 'approve_user' contact group in Xero ---
27
+ all_groups = accounting_api.get_contact_groups(xero_tenant_id).contact_groups
28
+ approved_group_info = next((g for g in all_groups if g.name == "approved_user"), None)
29
+
30
+ if not approved_group_info:
31
+ logger.warning("Xero contact group 'approve_user' not found. Skipping approval sync.")
32
+ return
33
+
34
+ # --- Step 2: Get the full list of contacts from that group ---
35
+ approved_group_full = accounting_api.get_contact_group(
36
+ xero_tenant_id, approved_group_info.contact_group_id
37
+ ).contact_groups[0]
38
+
39
+ # Create a set of approved contact IDs for efficient lookup
40
+ approved_contact_ids = {str(contact.contact_id) for contact in approved_group_full.contacts}
41
+ logger.info(f"Found {len(approved_contact_ids)} approved contacts in Xero group 'approve_user'.")
42
+
43
+ # --- Step 3: Iterate through all users in MongoDB ---
44
+ users_in_db = mongo.db.users.find({})
45
+ update_count = 0
46
+
47
+ for user in users_in_db:
48
+ user_xero_id = user.get('xero_contact_id')
49
+ current_approval_status = user.get('is_approved', False)
50
+
51
+ if not user_xero_id:
52
+ # Skip users who are not yet synced to Xero
53
+ continue
54
+
55
+ # --- Step 4: Check membership and update MongoDB if necessary ---
56
+ is_now_approved = str(user_xero_id) in approved_contact_ids
57
+
58
+ if is_now_approved != current_approval_status:
59
+ # Status has changed, update the user in MongoDB
60
+ mongo.db.users.update_one(
61
+ {'_id': user['_id']},
62
+ {'$set': {'is_approved': is_now_approved}}
63
+ )
64
+ logger.info(f"Updated approval status for user {user['email']} to {is_now_approved}.")
65
+ update_count += 1
66
+
67
+ logger.info(f"Xero approval sync complete. Updated {update_count} users.")
68
+
69
+ except Exception as e:
70
+ logger.error(f"An error occurred during Xero user approval sync: {e}")
71
+
72
+ # --- NEW FUNCTION: The core logic to create a Xero Contact ---
73
+ def create_xero_contact_async(app_context, registration_data):
74
+ """
75
+ Creates a contact in Xero, then adds the detailed registration info
76
+ as a history note. Runs in a background thread.
77
+ """
78
+ with app_context:
79
+ user_email = registration_data.get('email')
80
+ try:
81
+ xero_tenant_id = get_xero_tenant_id()
82
+ accounting_api = AccountingApi(api_client)
83
+
84
+ # --- Step 1: Map and Create the Core Contact ---
85
+
86
+ contact_person_name = registration_data.get('contactPerson', '')
87
+ first_name, last_name = (contact_person_name.split(' ', 1) + [''])[:2]
88
+
89
+ contact_person = ContactPerson(
90
+ first_name=first_name,
91
+ last_name=last_name,
92
+ email_address=user_email,
93
+ include_in_emails=True,
94
+ )
95
+ business_address = Address(
96
+ address_type='STREET',
97
+ address_line1=registration_data.get('businessAddress', 'N/A')
98
+ )
99
+ phone = Phone(
100
+ phone_type='DEFAULT',
101
+ phone_number=registration_data.get('phoneNumber')
102
+ )
103
+
104
+ # Create the main Contact object WITHOUT the notes field
105
+ contact_to_create = Contact(
106
+ name=registration_data.get('businessName'),
107
+ email_address=user_email,
108
+ contact_persons=[contact_person],
109
+ addresses=[business_address],
110
+ phones=[phone],
111
+ website=registration_data.get('companyWebsite'),
112
+ is_customer=True
113
+ # The 'notes' field is intentionally left out here
114
+ )
115
+
116
+ contacts_payload = Contacts(contacts=[contact_to_create])
117
+ created_contact_response = accounting_api.create_contacts(
118
+ xero_tenant_id, contacts=contacts_payload
119
+ )
120
+
121
+ # --- Step 2: Add Registration Details as a History Note ---
122
+
123
+ if not created_contact_response.contacts:
124
+ logger.error(f"Xero contact creation failed for {user_email}. No contact returned.")
125
+ return
126
+
127
+ # Get the ID of the contact we just created
128
+ new_contact_id = created_contact_response.contacts[0].contact_id
129
+ logger.info(f"Successfully created base Xero contact ({new_contact_id}) for user {user_email}. Now adding history.")
130
+ contact_groups = accounting_api.get_contact_groups(xero_tenant_id).contact_groups
131
+ pending_group = next((group for group in contact_groups if group.name == "pending_for_approval"), None)
132
+
133
+ if pending_group:
134
+ logger.info(f"Adding contact {new_contact_id} to pending_for_approval group {pending_group.contact_group_id}.")
135
+ # Prepare the group update payload with the new contact added
136
+ cg = ContactGroup(
137
+ contact_group_id=pending_group.contact_group_id,
138
+ contacts=[Contact(contact_id=new_contact_id)]
139
+ )
140
+ response = accounting_api.create_contact_group_contacts(
141
+ xero_tenant_id=xero_tenant_id,
142
+ contact_group_id=pending_group.contact_group_id,
143
+ contacts=Contacts(contacts=[Contact(contact_id=new_contact_id)])
144
+ )
145
+ logger.info(response)
146
+ logger.info(f"Contact {new_contact_id} added to contact group 'pending_for_approval'.")
147
+ else:
148
+ logger.warning("Contact group 'pending_for_approval' not found—skipping group update.")
149
+ # First, update our own database with the new ID
150
+ mongo.db.users.update_one(
151
+ {'email': user_email},
152
+ {'$set': {'xero_contact_id': new_contact_id}}
153
+ )
154
+
155
+ # Prepare the detailed notes for the history record
156
+ history_details = (
157
+ f"--- Client Application Details ---\n"
158
+ f"Business Name: {registration_data.get('businessName')}\n"
159
+ f"Contact Person: {registration_data.get('contactPerson')}\n"
160
+ f"Email: {registration_data.get('email')}\n"
161
+ f"Phone: {registration_data.get('phoneNumber')}\n"
162
+ f"Company Website: {registration_data.get('companyWebsite')}\n"
163
+ f"Business Address: {registration_data.get('businessAddress')}\n"
164
+ f"Business Type: {registration_data.get('businessType')}\n"
165
+ f"Years Operating: {registration_data.get('yearsOperating')}\n"
166
+ f"Number of Locations: {registration_data.get('numLocations')}\n"
167
+ f"Estimated Weekly Volume: {registration_data.get('estimatedVolume')}\n\n"
168
+
169
+ f"--- Logistics Information ---\n"
170
+ f"Preferred Delivery Times: {registration_data.get('preferredDeliveryTimes')}\n"
171
+ f"Has Loading Dock: {registration_data.get('hasLoadingDock')}\n"
172
+ f"Special Delivery Instructions: {registration_data.get('specialDeliveryInstructions')}\n"
173
+ f"Minimum Quantity Requirements: {registration_data.get('minQuantityRequirements')}\n\n"
174
+
175
+ f"--- Service & Billing ---\n"
176
+ f"Reason for Switching: {registration_data.get('reasonForSwitch')}\n"
177
+ f"Preferred Payment Method: {registration_data.get('paymentMethod')}\n\n"
178
+
179
+ f"--- Additional Notes ---\n"
180
+ f"{registration_data.get('additionalNotes')}"
181
+ )
182
+ #f"Supplier Priorities: {registration_data.get('priorities')}\n"
183
+
184
+
185
+ # Create the HistoryRecord payload
186
+ history_record = HistoryRecord(details=history_details)
187
+ history_payload = HistoryRecords(history_records=[history_record])
188
+
189
+ # Make the second API call to add the note
190
+ accounting_api.create_contact_history(
191
+ xero_tenant_id=xero_tenant_id,
192
+ contact_id=new_contact_id,
193
+ history_records=history_payload
194
+ )
195
+
196
+ logger.info(f"Successfully added registration history note to Xero contact {new_contact_id}.")
197
+
198
+ except Exception as e:
199
+ # This will catch errors from either the contact creation or history creation
200
+ logger.error(f"Failed during Xero contact/history creation for user {user_email}. Error: {e}")
201
+ # --- NEW FUNCTION: The trigger to run the contact creation in the background ---
202
+ def trigger_contact_creation(registration_data):
203
+ """Starts a background thread to create a Xero contact."""
204
+ try:
205
+ app_context = current_app.app_context()
206
+ thread = threading.Thread(
207
+ target=create_xero_contact_async,
208
+ args=(app_context, registration_data)
209
+ )
210
+ thread.daemon = True
211
+ thread.start()
212
+ logger.info(f"Started Xero contact creation thread for {registration_data.get('email')}")
213
+ except Exception as e:
214
+ logger.error(f"Failed to start Xero contact creation thread. Error: {e}")
215
+
216
+
217
+ # # --- Your existing Purchase Order functions below ---
218
+ # def create_xero_purchase_order_async(app_context, xero_tenant_id, order_details):
219
+ # with app_context:
220
+ # # try:
221
+ # accounting_api = AccountingApi(api_client)
222
+
223
+ # # --- MODIFICATION: Use the stored xero_contact_id ---
224
+ # user = mongo.db.users.find_one({"email": order_details['user_email']})
225
+ # contact_id = user.get('xero_contact_id')
226
+
227
+ # if not contact_id:
228
+ # logger.error(f"Cannot create PO. User {order_details['user_email']} does not have a Xero Contact ID.")
229
+ # # As a fallback, you could get the first contact, but it's better to be specific.
230
+ # # contact_id = accounting_api.get_contacts(xero_tenant_id).contacts[0].contact_id
231
+ # return
232
+
233
+ # # ... (rest of your PO logic is mostly the same)
234
+ # all_product_ids = [ObjectId(item['productId']) for item in order_details['items']]
235
+ # products_cursor = mongo.db.products.find({'_id': {'$in': all_product_ids}})
236
+ # product_map = {str(p['_id']): p for p in products_cursor}
237
+
238
+ # xero_items = accounting_api.get_items(xero_tenant_id).items
239
+ # xero_item_map = {item.name: item.code for item in xero_items}
240
+
241
+ # line_items = []
242
+ # for item in order_details['items']:
243
+ # product = product_map.get(item['productId'])
244
+ # if product:
245
+ # product_name = product.get('name', 'N/A')
246
+ # item_code = xero_item_map.get(product_name, "")
247
+ # line_items.append(
248
+ # LineItem(
249
+ # item_code=item_code,
250
+ # description=product_name+" ("+str(item["mode"]) + ")",
251
+ # quantity=item['quantity'],
252
+ # unit_amount=float(product.get('price', 0))
253
+ # )
254
+ # )
255
+
256
+ # if not line_items:
257
+ # logger.error("Xero PO failed: No valid line items for order %s", order_details['order_id'])
258
+ # return
259
+
260
+ # purchase_order = PurchaseOrder(
261
+ # contact=Contact(contact_id=contact_id), # <-- Use the specific contact_id
262
+ # date=datetime.now().date(),
263
+ # delivery_date=datetime.strptime(order_details["deliverydate"], "%Y-%m-%d").date(),
264
+ # delivery_address=order_details['delivery_address'],
265
+ # telephone=order_details['mobile_number'],
266
+ # reference=str(order_details['user_email']),
267
+ # line_items=line_items,
268
+ # status="AUTHORISED"
269
+ # )
270
+
271
+ # result = accounting_api.create_purchase_orders(
272
+ # xero_tenant_id, purchase_orders=PurchaseOrders(purchase_orders=[purchase_order])
273
+ # )
274
+ # logger.info("Created Xero PO for order ID: %s", order_details['order_id'])
275
+
276
+ # # except Exception as e:
277
+ # # logger.error("Failed to create Xero PO for order %s. Error: %s", order_details['order_id'], e)
278
+
279
+ def create_xero_purchase_order_async(app_context, xero_tenant_id, order_details):
280
+ with app_context:
281
+ # try:
282
+ accounting_api = AccountingApi(api_client)
283
+
284
+ all_product_ids = [ObjectId(item['productId']) for item in order_details['items']]
285
+ # Use mongo.db directly
286
+ products_cursor = mongo.db.products.find({'_id': {'$in': all_product_ids}})
287
+ product_map = {str(p['_id']): p for p in products_cursor}
288
+
289
+ xero_items = accounting_api.get_items(xero_tenant_id).items
290
+ xero_item_map = {item.name: item.code for item in xero_items}
291
+
292
+ line_items = []
293
+ for item in order_details['items']:
294
+ product = product_map.get(item['productId'])
295
+ if product:
296
+ product_name = product.get('name', 'N/A')
297
+ item_code = xero_item_map.get(product_name, "")
298
+ line_items.append(
299
+ LineItem(
300
+ item_code=item_code,
301
+ description=product_name+" ("+str(item["mode"]) + ")",
302
+ quantity=item['quantity'],
303
+ unit_amount=float(product.get('price', 0))
304
+ )
305
+ )
306
+
307
+ if not line_items:
308
+ logger.error("Xero PO failed: No valid line items for order %s", order_details['order_id'])
309
+ return
310
+ email_id=order_details['user_email']
311
+
312
+ contact_id = accounting_api.get_contacts(xero_tenant_id,where=f'EmailAddress=="{email_id}"').contacts[0].contact_id
313
+ print(order_details["deliverydate"])
314
+ purchase_order = PurchaseOrder(
315
+ contact=Contact(contact_id=contact_id),
316
+ date=datetime.now().date(),
317
+ delivery_date=datetime.strptime(order_details["deliverydate"], "%Y-%m-%d").date(),
318
+ delivery_address=order_details['delivery_address'],
319
+ telephone=order_details['mobile_number'],
320
+ reference=str(order_details['user_email']),
321
+ line_items=line_items,
322
+ status="SUBMITTED"
323
+ )
324
+
325
+ result = accounting_api.create_purchase_orders(
326
+ xero_tenant_id, purchase_orders=PurchaseOrders(purchase_orders=[purchase_order])
327
+ )
328
+ logger.info("Created Xero PO for order ID: %s", order_details['order_id'])
329
+
330
+ # except Exception as e:
331
+ # logger.error("Failed to create Xero PO for order %s. Error: %s", order_details['order_id'], e)
332
+
333
+ def trigger_po_creation(order_details):
334
+ # try:
335
+ from .xero_client import get_xero_tenant_id # Import locally to prevent cycles
336
+ xero_tenant_id = get_xero_tenant_id()
337
+ app_context = current_app.app_context()
338
+
339
+ thread = threading.Thread(
340
+ target=create_xero_purchase_order_async,
341
+ args=(app_context, xero_tenant_id, order_details)
342
+ )
343
+ thread.daemon = True
344
+ thread.start()
345
+ # except Exception as e:
346
+ # logger.error("Failed to start Xero PO creation thread for order %s. Error: %s", order_details.get('order_id'), e)
cache/.gitkeep ADDED
File without changes
cache/2029240f6d1128be89ddc32729463129 ADDED
Binary file (9 Bytes). View file
 
cache/7b19acab5f13885d95f709e2dcbd5729 ADDED
Binary file (3.69 kB). View file
 
default_settings.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ import os
3
+ from os.path import dirname, join
4
+
5
+ SECRET_KEY = os.urandom(16)
6
+ # configure file based session
7
+ SESSION_TYPE = "filesystem"
8
+ SESSION_FILE_DIR = join(dirname(__file__), "cache")
9
+
10
+ # configure flask app for local development
11
+ ENV = "development"
flask_session/2029240f6d1128be89ddc32729463129 ADDED
Binary file (9 Bytes). View file
 
flask_session/5dd30f329f4495f0adf98d75dcf44c99 ADDED
Binary file (140 Bytes). View file
 
flask_session/aa71dde20eaf768ca7e5f90a25563ea6 ADDED
Binary file (179 Bytes). View file
 
flask_session/d0efef4e2c8720847d7a47ac360d9d46 ADDED
Binary file (153 Bytes). View file
 
hfapis.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from huggingface_hub import HfApi
2
+ from dotenv import dotenv_values
3
+
4
+ # Load .env variables (dotenv_values returns a dict)
5
+ env_vars = dotenv_values(r'C:\Users\Vaibhav Arora\Documents\MyExperimentsandCodes\APPS_WEBSITES\CANADA_WHOLESALE_PROJECT\GITHUB_REPOS\mvp-vue\wholesale-grocery-app\AIAPPS\.env')
6
+
7
+ # Initialize API
8
+ api = HfApi(token=env_vars.get("HF_TOKEN"))
9
+
10
+ # Space repo ID (username_or_org/space_name)
11
+ REPO_ID = "akiko19191/randomisedbackend2"
12
+
13
+ # Loop through all variables in .env and add them as Space secrets
14
+ for key, value in env_vars.items():
15
+ print(value)
16
+ if value is None:
17
+ continue # skip empty entries
18
+ print(f"Setting secret: {key}")
19
+ api.add_space_secret(
20
+ repo_id=REPO_ID,
21
+ key=key,
22
+ value=value
23
+ )
24
+
25
+ print("✅ All .env variables uploaded as Hugging Face Space secrets.")
logging_settings.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+
3
+ default_settings = {
4
+ "version": 1,
5
+ "formatters": {
6
+ "default": {"format": "[%(asctime)s] %(levelname)s in %(module)s: %(message)s"},
7
+ "verbose": {
8
+ "format": "%(asctime)s | %(levelname)s [%(name)s.%(filename)s:%(lineno)s] %(message)s",
9
+ "datefmt": "%Y-%m-%d %H:%M:%S%z",
10
+ },
11
+ },
12
+ "handlers": {
13
+ "console": {
14
+ "class": "logging.StreamHandler",
15
+ "stream": "ext://flask.logging.wsgi_errors_stream",
16
+ "formatter": "verbose",
17
+ "level": "DEBUG",
18
+ }
19
+ },
20
+ "loggers": {
21
+ "requests_oauthlib": {"handlers": ["console"], "level": "DEBUG"},
22
+ "xero_python": {"handlers": ["console"], "level": "DEBUG"},
23
+ "urllib3": {"handlers": ["console"], "level": "DEBUG"},
24
+ },
25
+ # "root": {"level": "DEBUG", "handlers": ["console"]},
26
+ }
requirements.txt ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ flask
2
+ flask-session
3
+ flask-oauthlib
4
+ xero-python
5
+ flask-jwt-extended
6
+ flask-cors
7
+ Flask-PyMongo
8
+ flask-session
9
+ Flask-OAuthlib
10
+ flask-bcrypt
11
+ google-api-python-client
12
+ google-genai
13
+ pydantic
14
+ sib-api-v3-sdk
15
+ google-generativeai
16
+ twilio
run.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ from app import create_app
2
+ import os
3
+ os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
4
+ app = create_app()
5
+
6
+ if __name__ == '__main__':
7
+ app.run(host="0.0.0.0",debug=False, port=7860)
search_engine.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from googleapiclient.discovery import build
2
+ from dotenv import load_dotenv
3
+ load_dotenv(r'C:\Users\Vaibhav Arora\Documents\MyExperimentsandCodes\APPS_WEBSITES\CANADA_WHOLESALE_PROJECT\GITHUB_REPOS\mvp-vue\wholesale-grocery-app\AIAPPS\.env')
4
+ from google import genai
5
+ from pydantic import BaseModel
6
+ import ast
7
+ import os
8
+ import json
9
+ import random
10
+ import time
11
+ class Categorise(BaseModel):
12
+ category: str
13
+ client = genai.Client(api_key=random.choice(json.loads(os.getenv("GEMINI_KEY_LIST"))))
14
+
15
+ def categorise(product):
16
+ try:
17
+
18
+
19
+
20
+ response = client.models.generate_content(
21
+ model="gemini-2.5-flash-lite-preview-06-17",
22
+ contents=f"Categorise this product:{product} , into one of the following categories: `Fruits,Vegetables,Bakery,Others`",
23
+ config={
24
+ "response_mime_type": "application/json",
25
+ "response_schema": list[Categorise],
26
+ },
27
+ )
28
+
29
+ except:
30
+ time.sleep(2)
31
+ response = client.models.generate_content(
32
+ model="gemini-2.5-flash",
33
+ contents=f"Categorise this product:{product} , into one of the following categories: `Fruits,Vegetables,Bakery,Others`",
34
+ config={
35
+ "response_mime_type": "application/json",
36
+ "response_schema": list[Categorise],
37
+ },
38
+ )
39
+ return ast.literal_eval(response.text)[0]["category"]
40
+
41
+ def search_images(query: str, api_key: str, cse_id: str,no) -> dict | None:
42
+ """
43
+ Performs an image search using the Google Custom Search API.
44
+ """
45
+ print(f"Searching for images with query: '{query}'...")
46
+ try:
47
+ service = build("customsearch", "v1", developerKey=api_key)
48
+ result = service.cse().list(
49
+ q=query,
50
+ cx=cse_id,
51
+ searchType='image',
52
+ num=no
53
+ ).execute()
54
+ print("Search successful.")
55
+ return result
56
+ except Exception as e:
57
+ print(f"An error occurred during Google Search: {e}")
58
+ return None
59
+
60
+ def search_and_filter_images(query,no=2):
61
+
62
+
63
+ search_results = search_images(query, os.getenv("CSE_API_KEY"), os.getenv("CSE_ID"),no)
64
+
65
+ if search_results and 'items' in search_results:
66
+ top_10_items = search_results['items']
67
+ print(f"Found {len(top_10_items)} image results. Downloading them...")
68
+
69
+ image_files_for_llm = []
70
+ downloaded_filenames = []
71
+
72
+ for i, item in enumerate(top_10_items):
73
+ image_url = item.get('link')
74
+ if not image_url:
75
+ continue
76
+
77
+ file_extension = os.path.splitext(image_url.split("?")[0])[-1]
78
+ if not file_extension:
79
+ file_extension = ".unknown" # Default extension
80
+ if not file_extension in [".jpeg", ".jpg", ".png", ".gif", ".bmp", ".webp"]:
81
+ continue
82
+
83
+
84
+ image_files_for_llm.append({
85
+ "type": "image_url",
86
+ "image_url": f"{image_url}"
87
+ })
88
+ # print(image_files_for_llm)
89
+ return (image_files_for_llm)
90
+
twillio_gemini_api.py ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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)
156
+
157
+ except Exception as e:
158
+ print(f"An error occurred with the Gemini API: {e}")
159
+ # Send a user-friendly error message
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)
utils.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ import json
3
+ import uuid
4
+ from datetime import datetime, date
5
+ from decimal import Decimal
6
+
7
+ from xero_python.api_client.serializer import serialize
8
+
9
+
10
+ class JSONEncoder(json.JSONEncoder):
11
+ def default(self, o):
12
+ if isinstance(o, datetime):
13
+ return o.isoformat()
14
+ if isinstance(o, date):
15
+ return o.isoformat()
16
+ if isinstance(o, (uuid.UUID, Decimal)):
17
+ return str(o)
18
+ return super(JSONEncoder, self).default(o)
19
+
20
+
21
+ def parse_json(data):
22
+ return json.loads(data, parse_float=Decimal)
23
+
24
+
25
+ def serialize_model(model):
26
+ return jsonify(serialize(model))
27
+
28
+
29
+ def jsonify(data):
30
+ return json.dumps(data, sort_keys=True, indent=4, cls=JSONEncoder)