Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files- Dockerfile +17 -0
- __pycache__/search_engine.cpython-311.pyc +0 -0
- __pycache__/utils.cpython-311.pyc +0 -0
- app/Company_Info.py +29 -0
- app/__init__.py +66 -0
- app/__pycache__/Company_Info.cpython-311.pyc +0 -0
- app/__pycache__/__init__.cpython-311.pyc +0 -0
- app/__pycache__/ai_features.cpython-311.pyc +0 -0
- app/__pycache__/api.cpython-311.pyc +0 -0
- app/__pycache__/config.cpython-311.pyc +0 -0
- app/__pycache__/email_utils.cpython-311.pyc +0 -0
- app/__pycache__/extensions.cpython-311.pyc +0 -0
- app/__pycache__/general_utils.cpython-311.pyc +0 -0
- app/__pycache__/models.cpython-311.pyc +0 -0
- app/__pycache__/page_features.cpython-311.pyc +0 -0
- app/__pycache__/xero_client.cpython-311.pyc +0 -0
- app/__pycache__/xero_routes.cpython-311.pyc +0 -0
- app/__pycache__/xero_utils.cpython-311.pyc +0 -0
- app/ai_features.py +697 -0
- app/api.py +392 -0
- app/config.py +23 -0
- app/email_utils.py +214 -0
- app/extensions.py +13 -0
- app/general_utils.py +70 -0
- app/models.py +7 -0
- app/page_features.py +167 -0
- app/templates/base.html +16 -0
- app/templates/code.html +16 -0
- app/xero_client.py +66 -0
- app/xero_routes.py +104 -0
- app/xero_utils.py +346 -0
- cache/.gitkeep +0 -0
- cache/2029240f6d1128be89ddc32729463129 +0 -0
- cache/7b19acab5f13885d95f709e2dcbd5729 +0 -0
- default_settings.py +11 -0
- flask_session/2029240f6d1128be89ddc32729463129 +0 -0
- flask_session/5dd30f329f4495f0adf98d75dcf44c99 +0 -0
- flask_session/aa71dde20eaf768ca7e5f90a25563ea6 +0 -0
- flask_session/d0efef4e2c8720847d7a47ac360d9d46 +0 -0
- hfapis.py +25 -0
- logging_settings.py +26 -0
- requirements.txt +16 -0
- run.py +7 -0
- search_engine.py +90 -0
- twillio_gemini_api.py +168 -0
- 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)
|