Spaces:
Sleeping
Sleeping
Update main.py
Browse files
main.py
CHANGED
|
@@ -540,7 +540,7 @@ def user_manage_listing(listing_id):
|
|
| 540 |
if field in data and data[field] != listing_data.get(field):
|
| 541 |
update_payload[field] = data[field]
|
| 542 |
requires_reapproval = True
|
| 543 |
-
|
| 544 |
# Allow user to change status to inactive/closed without re-approval
|
| 545 |
if 'status' in data and data['status'] in ['inactive', 'closed']:
|
| 546 |
update_payload['status'] = data['status']
|
|
@@ -550,7 +550,7 @@ def user_manage_listing(listing_id):
|
|
| 550 |
update_payload['status'] = 'pending_approval'
|
| 551 |
else: # If no critical fields changed, allow status update if valid
|
| 552 |
update_payload['status'] = data['status'] # e.g., from inactive back to active if no critical changes
|
| 553 |
-
|
| 554 |
if requires_reapproval and update_payload.get('status') != 'pending_approval':
|
| 555 |
update_payload['status'] = 'pending_approval' # Ensure pending approval if critical fields changed
|
| 556 |
|
|
@@ -559,7 +559,7 @@ def user_manage_listing(listing_id):
|
|
| 559 |
|
| 560 |
update_payload['last_updated_at'] = datetime.now(timezone.utc).isoformat()
|
| 561 |
listing_ref.update(update_payload)
|
| 562 |
-
|
| 563 |
message = 'Listing updated successfully.'
|
| 564 |
if requires_reapproval:
|
| 565 |
message += ' Listing status set to pending_approval due to significant changes.'
|
|
@@ -640,13 +640,13 @@ def admin_remove_listing(listing_id):
|
|
| 640 |
|
| 641 |
if not listing_data or not isinstance(listing_data, dict):
|
| 642 |
return jsonify({'error': 'Listing not found.'}), 404
|
| 643 |
-
|
| 644 |
lister_id = listing_data.get('lister_id')
|
| 645 |
listing_type = listing_data.get('listing_type', 'item')
|
| 646 |
crop_type = listing_data.get('crop_type', 'N/A')
|
| 647 |
|
| 648 |
listing_ref.delete()
|
| 649 |
-
|
| 650 |
if lister_id:
|
| 651 |
_send_system_notification(lister_id, f"Your {listing_type} listing for '{crop_type}' (ID: {listing_id}) has been removed by an administrator.", "listing_removed_by_admin", f"/my-listings")
|
| 652 |
|
|
@@ -660,12 +660,14 @@ def admin_remove_listing(listing_id):
|
|
| 660 |
@app.route('/api/deals/propose', methods=['POST'])
|
| 661 |
def propose_deal():
|
| 662 |
auth_header = request.headers.get('Authorization')
|
| 663 |
-
|
|
|
|
|
|
|
| 664 |
try:
|
| 665 |
logger.info(f"Backend /api/deals/propose received headers: {request.headers}")
|
| 666 |
logger.info(f"Backend /api/deals/propose received raw data: {request.get_data(as_text=True)}")
|
| 667 |
|
| 668 |
-
|
| 669 |
if not FIREBASE_INITIALIZED:
|
| 670 |
return jsonify({'error': 'Server configuration error: Firebase not ready.'}), 503
|
| 671 |
|
|
@@ -674,10 +676,29 @@ def propose_deal():
|
|
| 674 |
logger.error("Backend /api/deals/propose: No JSON data received or failed to parse.")
|
| 675 |
return jsonify({'error': 'Invalid JSON data received.'}), 400
|
| 676 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 677 |
listing_id = data.get('listing_id')
|
| 678 |
-
# 'quantity' from frontend is the quantity the proposer wants to transact
|
| 679 |
proposed_quantity_by_user = data.get('quantity')
|
| 680 |
-
# 'price' from frontend is the price PER UNIT the proposer is offering/asking
|
| 681 |
proposed_price_per_unit_by_user = data.get('price')
|
| 682 |
notes = data.get('notes', "")
|
| 683 |
|
|
@@ -706,8 +727,8 @@ def propose_deal():
|
|
| 706 |
original_lister_id = listing_data.get('lister_id')
|
| 707 |
listing_type = listing_data.get('listing_type') # 'produce' or 'demand'
|
| 708 |
|
| 709 |
-
if original_lister_id ==
|
| 710 |
-
logger.error(f"Backend /api/deals/propose: User {
|
| 711 |
return jsonify({'error': 'Cannot propose a deal to your own listing/demand.'}), 400
|
| 712 |
|
| 713 |
deal_farmer_id = None
|
|
@@ -716,9 +737,13 @@ def propose_deal():
|
|
| 716 |
notification_recipient_id = original_lister_id # The other party
|
| 717 |
|
| 718 |
if listing_type == 'produce':
|
| 719 |
-
# Current user (proposer
|
|
|
|
|
|
|
|
|
|
|
|
|
| 720 |
deal_farmer_id = original_lister_id
|
| 721 |
-
deal_buyer_id =
|
| 722 |
deal_notes_prefix = "Buyer's proposal: "
|
| 723 |
|
| 724 |
available_quantity = listing_data.get('quantity', 0)
|
|
@@ -727,8 +752,8 @@ def propose_deal():
|
|
| 727 |
return jsonify({'error': f'Proposed quantity ({proposed_quantity_by_user}) exceeds available quantity ({available_quantity}) for the listing.'}), 400
|
| 728 |
|
| 729 |
elif listing_type == 'demand':
|
| 730 |
-
# Current user (proposer
|
| 731 |
-
deal_farmer_id =
|
| 732 |
deal_buyer_id = original_lister_id
|
| 733 |
deal_notes_prefix = "Farmer's offer against demand: "
|
| 734 |
|
|
@@ -744,7 +769,7 @@ def propose_deal():
|
|
| 744 |
deal_id = str(uuid.uuid4())
|
| 745 |
deal_data_to_set = {
|
| 746 |
'deal_id': deal_id,
|
| 747 |
-
'proposer_id':
|
| 748 |
'listing_id': listing_id,
|
| 749 |
'farmer_id': deal_farmer_id,
|
| 750 |
'buyer_id': deal_buyer_id,
|
|
@@ -755,32 +780,57 @@ def propose_deal():
|
|
| 755 |
'created_at': datetime.now(timezone.utc).isoformat(),
|
| 756 |
'chat_room_id': f"deal_{deal_id}"
|
| 757 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 758 |
db.reference(f'deals/{deal_id}', app=db_app).set(deal_data_to_set)
|
| 759 |
|
| 760 |
_send_system_notification(
|
| 761 |
notification_recipient_id,
|
| 762 |
-
f"You have a new proposal/offer from {
|
| 763 |
"new_deal_proposal",
|
| 764 |
f"/deals/{deal_id}"
|
| 765 |
)
|
| 766 |
-
logger.info(f"Backend /api/deals/propose: Deal {deal_id} created successfully by UID {
|
| 767 |
return jsonify({'success': True, 'message': 'Proposal/Offer submitted successfully.', 'deal': deal_data_to_set}), 201
|
| 768 |
|
| 769 |
except Exception as e:
|
| 770 |
# Log the original error before passing to generic handler
|
| 771 |
-
logger.error(f"Backend /api/deals/propose: Unhandled exception for UID {
|
| 772 |
-
return handle_route_errors(e, uid_context=
|
| 773 |
|
| 774 |
@app.route('/api/deals/<deal_id>/respond', methods=['POST'])
|
| 775 |
def respond_to_deal(deal_id):
|
| 776 |
auth_header = request.headers.get('Authorization')
|
| 777 |
-
|
|
|
|
|
|
|
| 778 |
try:
|
| 779 |
-
|
| 780 |
if not FIREBASE_INITIALIZED: return jsonify({'error': 'Server configuration error'}), 503
|
| 781 |
|
| 782 |
data = request.get_json()
|
| 783 |
response_action = data.get('action') # 'accept' or 'reject'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 784 |
|
| 785 |
if response_action not in ['accept', 'reject']:
|
| 786 |
return jsonify({'error': 'Invalid action. Must be "accept" or "reject".'}), 400
|
|
@@ -799,23 +849,23 @@ def respond_to_deal(deal_id):
|
|
| 799 |
# Authorization Check: Who is allowed to respond?
|
| 800 |
can_respond = False
|
| 801 |
if current_deal_status == 'proposed':
|
| 802 |
-
if proposer_id == buyer_id_in_deal and
|
| 803 |
can_respond = True
|
| 804 |
-
elif proposer_id == farmer_id_in_deal and
|
| 805 |
can_respond = True
|
| 806 |
# Add other statuses if a different party needs to respond (e.g., after admin action)
|
| 807 |
|
| 808 |
if not can_respond:
|
| 809 |
-
logger.warning(f"UID {
|
| 810 |
return jsonify({'error': 'Not authorized to respond to this deal at its current state.'}), 403
|
| 811 |
|
| 812 |
update_time = datetime.now(timezone.utc).isoformat()
|
| 813 |
|
| 814 |
# Determine the other party for notification
|
| 815 |
other_party_id = None
|
| 816 |
-
if
|
| 817 |
other_party_id = buyer_id_in_deal
|
| 818 |
-
elif
|
| 819 |
other_party_id = farmer_id_in_deal
|
| 820 |
|
| 821 |
listing_id = deal_data.get('listing_id')
|
|
@@ -824,9 +874,13 @@ def respond_to_deal(deal_id):
|
|
| 824 |
listing_data_for_notif = db.reference(f'listings/{listing_id}', app=db_app).get() or {}
|
| 825 |
crop_type_for_notif = listing_data_for_notif.get('crop_type', 'your listing/demand')
|
| 826 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 827 |
if response_action == 'accept':
|
| 828 |
# Quantity check (if applicable, e.g., if responding to a proposal against a produce listing)
|
| 829 |
-
if proposer_id == buyer_id_in_deal and
|
| 830 |
if listing_id and isinstance(listing_data_for_notif, dict):
|
| 831 |
available_quantity = listing_data_for_notif.get('quantity', 0)
|
| 832 |
proposed_quantity = deal_data.get('proposed_quantity', 0)
|
|
@@ -840,25 +894,21 @@ def respond_to_deal(deal_id):
|
|
| 840 |
_send_system_notification(other_party_id, f"Your deal proposal for '{crop_type_for_notif}' could not be accepted due to insufficient stock.", "deal_status_update", f"/deals/{deal_id}")
|
| 841 |
return jsonify({'success': False, 'error': 'Deal could not be accepted. Listing quantity is no longer sufficient.'}), 409
|
| 842 |
|
| 843 |
-
# If a buyer accepts a farmer's counter-offer, or farmer accepts buyer's initial proposal
|
| 844 |
-
# The deal moves to 'accepted_by_farmer' or 'accepted_by_buyer' then to admin.
|
| 845 |
-
# For simplicity, let's assume both lead to a state needing admin approval.
|
| 846 |
-
# We need a clear "who accepted" field.
|
| 847 |
-
|
| 848 |
accepted_by_field = ""
|
| 849 |
new_status = ""
|
| 850 |
-
if
|
| 851 |
accepted_by_field = "farmer_accepted_at"
|
| 852 |
new_status = "accepted_by_farmer" # Farmer accepts buyer's proposal
|
| 853 |
notification_message_to_other_party = f"Your deal proposal for '{crop_type_for_notif}' has been ACCEPTED by the farmer. It is now pending admin approval."
|
| 854 |
-
elif
|
| 855 |
accepted_by_field = "buyer_accepted_at"
|
| 856 |
new_status = "accepted_by_buyer" # Buyer accepts farmer's offer/counter
|
| 857 |
notification_message_to_other_party = f"Your offer/counter-offer for '{crop_type_for_notif}' has been ACCEPTED by the buyer. It is now pending admin approval."
|
| 858 |
else: # Should not happen due to auth check
|
| 859 |
return jsonify({'error': 'Internal error determining accepter role.'}), 500
|
| 860 |
|
| 861 |
-
|
|
|
|
| 862 |
|
| 863 |
if other_party_id:
|
| 864 |
_send_system_notification(other_party_id, notification_message_to_other_party, "deal_status_update", f"/deals/{deal_id}")
|
|
@@ -867,31 +917,32 @@ def respond_to_deal(deal_id):
|
|
| 867 |
admins_ref = db.reference('users', app=db_app).order_by_child('is_admin').equal_to(True).get()
|
| 868 |
if admins_ref:
|
| 869 |
for admin_id_loop, _ in admins_ref.items():
|
| 870 |
-
_send_system_notification(admin_id_loop, f"Deal ID {deal_id} for '{crop_type_for_notif}' has been accepted by {
|
| 871 |
|
| 872 |
-
return jsonify({'success': True, 'message': f'Deal accepted by {("farmer" if
|
| 873 |
|
| 874 |
elif response_action == 'reject':
|
| 875 |
rejected_by_field = ""
|
| 876 |
new_status = ""
|
| 877 |
-
if
|
| 878 |
rejected_by_field = "farmer_rejected_at" # Or just 'responded_at'
|
| 879 |
new_status = "rejected_by_farmer"
|
| 880 |
notification_message_to_other_party = f"Your deal proposal for '{crop_type_for_notif}' has been REJECTED by the farmer."
|
| 881 |
-
elif
|
| 882 |
rejected_by_field = "buyer_rejected_at"
|
| 883 |
new_status = "rejected_by_buyer"
|
| 884 |
notification_message_to_other_party = f"Your offer/counter-offer for '{crop_type_for_notif}' has been REJECTED by the buyer."
|
| 885 |
else: # Should not happen
|
| 886 |
return jsonify({'error': 'Internal error determining rejector role.'}), 500
|
| 887 |
|
| 888 |
-
|
|
|
|
| 889 |
if other_party_id:
|
| 890 |
_send_system_notification(other_party_id, notification_message_to_other_party, "deal_status_update", f"/deals/{deal_id}")
|
| 891 |
-
return jsonify({'success': True, 'message': f'Deal rejected by {("farmer" if
|
| 892 |
|
| 893 |
except Exception as e:
|
| 894 |
-
return handle_route_errors(e, uid_context=
|
| 895 |
|
| 896 |
@app.route('/api/deals/<deal_id>/complete', methods=['POST'])
|
| 897 |
def complete_deal_by_admin(deal_id):
|
|
@@ -1020,12 +1071,12 @@ def admin_remove_deal(deal_id):
|
|
| 1020 |
|
| 1021 |
if not deal_data or not isinstance(deal_data, dict):
|
| 1022 |
return jsonify({'error': 'Deal not found.'}), 404
|
| 1023 |
-
|
| 1024 |
farmer_id = deal_data.get('farmer_id')
|
| 1025 |
buyer_id = deal_data.get('buyer_id')
|
| 1026 |
transporter_id = deal_data.get('assigned_transporter_id')
|
| 1027 |
listing_id = deal_data.get('listing_id')
|
| 1028 |
-
|
| 1029 |
crop_type = "N/A"
|
| 1030 |
if listing_id:
|
| 1031 |
listing_details = db.reference(f'listings/{listing_id}', app=db_app).get()
|
|
@@ -1033,7 +1084,7 @@ def admin_remove_deal(deal_id):
|
|
| 1033 |
crop_type = listing_details.get('crop_type', 'N/A')
|
| 1034 |
|
| 1035 |
deal_ref.delete()
|
| 1036 |
-
|
| 1037 |
message_to_parties = f"Deal ID {deal_id} for '{crop_type}' has been removed by an administrator."
|
| 1038 |
if farmer_id:
|
| 1039 |
_send_system_notification(farmer_id, message_to_parties, "deal_removed_by_admin", f"/my-deals")
|
|
@@ -1047,114 +1098,7 @@ def admin_remove_deal(deal_id):
|
|
| 1047 |
except Exception as e:
|
| 1048 |
return handle_route_errors(e, uid_context=admin_uid)
|
| 1049 |
|
| 1050 |
-
#
|
| 1051 |
-
@app.route('/api/admin/deals/create-manual', methods=['POST'])
|
| 1052 |
-
def admin_create_manual_deal():
|
| 1053 |
-
auth_header = request.headers.get('Authorization')
|
| 1054 |
-
admin_uid = None
|
| 1055 |
-
try:
|
| 1056 |
-
admin_uid, _, _ = verify_admin_or_facilitator(auth_header)
|
| 1057 |
-
if not FIREBASE_INITIALIZED: return jsonify({'error': 'Server configuration error: Firebase not ready.'}), 503
|
| 1058 |
-
|
| 1059 |
-
data = request.get_json()
|
| 1060 |
-
farmer_id = data.get('farmer_id')
|
| 1061 |
-
buyer_id = data.get('buyer_id')
|
| 1062 |
-
listing_id = data.get('listing_id')
|
| 1063 |
-
quantity = data.get('quantity')
|
| 1064 |
-
price_per_unit = data.get('price_per_unit')
|
| 1065 |
-
notes = data.get('notes', "Admin-created deal.")
|
| 1066 |
-
|
| 1067 |
-
if not all([farmer_id, buyer_id, listing_id, quantity is not None, price_per_unit is not None]):
|
| 1068 |
-
return jsonify({'error': 'farmer_id, buyer_id, listing_id, quantity, and price_per_unit are required.'}), 400
|
| 1069 |
-
|
| 1070 |
-
try:
|
| 1071 |
-
quantity = int(quantity)
|
| 1072 |
-
price_per_unit = float(price_per_unit)
|
| 1073 |
-
if quantity <= 0 or price_per_unit <= 0:
|
| 1074 |
-
raise ValueError("Quantity and price must be positive numbers.")
|
| 1075 |
-
except ValueError as ve:
|
| 1076 |
-
return jsonify({'error': f'Invalid quantity or price: {str(ve)}'}), 400
|
| 1077 |
-
|
| 1078 |
-
# Verify farmer and buyer exist and have correct roles or are whatsapp_managed
|
| 1079 |
-
farmer_profile = db.reference(f'users/{farmer_id}', app=db_app).get()
|
| 1080 |
-
buyer_profile = db.reference(f'users/{buyer_id}', app=db_app).get()
|
| 1081 |
-
|
| 1082 |
-
if not farmer_profile or (not farmer_profile.get('roles', {}).get('farmer') and farmer_profile.get('account_type') != 'whatsapp_managed'):
|
| 1083 |
-
return jsonify({'error': 'Farmer ID invalid or user is not a farmer/WhatsApp farmer.'}), 400
|
| 1084 |
-
if not buyer_profile or (not buyer_profile.get('roles', {}).get('buyer') and farmer_profile.get('account_type') != 'whatsapp_managed'):
|
| 1085 |
-
return jsonify({'error': 'Buyer ID invalid or user is not a buyer/WhatsApp buyer.'}), 400
|
| 1086 |
-
|
| 1087 |
-
# Verify listing
|
| 1088 |
-
listing_ref = db.reference(f'listings/{listing_id}', app=db_app)
|
| 1089 |
-
listing_data = listing_ref.get()
|
| 1090 |
-
|
| 1091 |
-
if not listing_data or listing_data.get('status') != 'active':
|
| 1092 |
-
return jsonify({'error': 'Target listing not found or not active.'}), 404
|
| 1093 |
-
|
| 1094 |
-
# Ensure the listing's lister matches the farmer/buyer in the deal
|
| 1095 |
-
if listing_data.get('listing_type') == 'produce' and listing_data.get('lister_id') != farmer_id:
|
| 1096 |
-
return jsonify({'error': 'Produce listing lister_id does not match provided farmer_id.'}), 400
|
| 1097 |
-
if listing_data.get('listing_type') == 'demand' and listing_data.get('lister_id') != buyer_id:
|
| 1098 |
-
return jsonify({'error': 'Demand listing lister_id does not match provided buyer_id.'}), 400
|
| 1099 |
-
|
| 1100 |
-
# Quantity check for produce listings
|
| 1101 |
-
if listing_data.get('listing_type') == 'produce':
|
| 1102 |
-
available_quantity = listing_data.get('quantity', 0)
|
| 1103 |
-
if quantity > available_quantity:
|
| 1104 |
-
return jsonify({'error': f'Proposed quantity ({quantity}) exceeds available quantity ({available_quantity}) for the listing.'}), 400
|
| 1105 |
-
|
| 1106 |
-
deal_id = str(uuid.uuid4())
|
| 1107 |
-
deal_data_to_set = {
|
| 1108 |
-
'deal_id': deal_id,
|
| 1109 |
-
'proposer_id': admin_uid, # Admin is the proposer in this context
|
| 1110 |
-
'listing_id': listing_id,
|
| 1111 |
-
'farmer_id': farmer_id,
|
| 1112 |
-
'buyer_id': buyer_id,
|
| 1113 |
-
'proposed_quantity': quantity,
|
| 1114 |
-
'proposed_price': price_per_unit,
|
| 1115 |
-
'deal_notes': notes,
|
| 1116 |
-
'status': 'active', # Admin-created deals are active immediately
|
| 1117 |
-
'created_at': datetime.now(timezone.utc).isoformat(),
|
| 1118 |
-
'admin_created_by': admin_uid,
|
| 1119 |
-
'chat_room_id': f"deal_{deal_id}"
|
| 1120 |
-
}
|
| 1121 |
-
db.reference(f'deals/{deal_id}', app=db_app).set(deal_data_to_set)
|
| 1122 |
-
|
| 1123 |
-
# Update listing quantity if it's a produce listing
|
| 1124 |
-
if listing_data.get('listing_type') == 'produce':
|
| 1125 |
-
def update_listing_quantity_transaction(current_listing_data_tx):
|
| 1126 |
-
if not current_listing_data_tx or not isinstance(current_listing_data_tx, dict):
|
| 1127 |
-
logger.error(f"AdminCreateManualDeal: Listing {listing_id} not found or malformed during transaction for deal {deal_id}.")
|
| 1128 |
-
return current_listing_data_tx
|
| 1129 |
-
current_listing_quantity = current_listing_data_tx.get('quantity', 0)
|
| 1130 |
-
new_listing_quantity = current_listing_quantity - quantity
|
| 1131 |
-
updates = {}
|
| 1132 |
-
if new_listing_quantity <= 0:
|
| 1133 |
-
updates['quantity'] = 0
|
| 1134 |
-
updates['status'] = 'closed'
|
| 1135 |
-
else:
|
| 1136 |
-
updates['quantity'] = new_listing_quantity
|
| 1137 |
-
current_listing_data_tx.update(updates)
|
| 1138 |
-
return current_listing_data_tx
|
| 1139 |
-
|
| 1140 |
-
transaction_result = listing_ref.transaction(update_listing_quantity_transaction)
|
| 1141 |
-
if transaction_result is None and listing_ref.get() is not None:
|
| 1142 |
-
logger.warning(f"AdminCreateManualDeal: Transaction to update listing {listing_id} for deal {deal_id} was aborted or listing became null. Deal created, but listing quantity may not be updated.")
|
| 1143 |
-
elif listing_ref.get() is None:
|
| 1144 |
-
logger.warning(f"AdminCreateManualDeal: Listing {listing_id} not found for deal {deal_id}. Deal created, but listing quantity not updated.")
|
| 1145 |
-
else:
|
| 1146 |
-
logger.info(f"AdminCreateManualDeal: Listing {listing_id} quantity updated via transaction for deal {deal_id}.")
|
| 1147 |
-
|
| 1148 |
-
|
| 1149 |
-
# Notify farmer and buyer
|
| 1150 |
-
crop_type_for_notif = listing_data.get('crop_type', 'item')
|
| 1151 |
-
_send_system_notification(farmer_id, f"An administrator has created a deal for you regarding '{crop_type_for_notif}' (Deal ID: {deal_id}). Quantity: {quantity}, Price/Unit: {price_per_unit}.", "admin_created_deal", f"/deals/{deal_id}")
|
| 1152 |
-
_send_system_notification(buyer_id, f"An administrator has created a deal for you regarding '{crop_type_for_notif}' (Deal ID: {deal_id}). Quantity: {quantity}, Price/Unit: {price_per_unit}.", "admin_created_deal", f"/deals/{deal_id}")
|
| 1153 |
-
|
| 1154 |
-
return jsonify({'success': True, 'message': 'Deal created manually by admin.', 'deal': deal_data_to_set}), 201
|
| 1155 |
-
|
| 1156 |
-
except Exception as e:
|
| 1157 |
-
return handle_route_errors(e, uid_context=admin_uid)
|
| 1158 |
|
| 1159 |
|
| 1160 |
#--- END OF MODIFIED DEAL MANAGEMENT SECTION ---
|
|
@@ -1808,30 +1752,30 @@ def mark_notification_read(notification_id):
|
|
| 1808 |
# Helper for AI chat to fetch platform data
|
| 1809 |
def _fetch_platform_data_for_chat(query):
|
| 1810 |
if not FIREBASE_INITIALIZED: return "Firebase not ready.", False
|
| 1811 |
-
|
| 1812 |
listings_ref = db.reference('listings', app=db_app).order_by_child('status').equal_to('active')
|
| 1813 |
all_active_listings = listings_ref.get() or {}
|
| 1814 |
-
|
| 1815 |
relevant_listings = []
|
| 1816 |
query_lower = query.lower()
|
| 1817 |
-
|
| 1818 |
for lid, ldata in all_active_listings.items():
|
| 1819 |
if not ldata: continue
|
| 1820 |
crop_type = ldata.get('crop_type', '').lower()
|
| 1821 |
location = ldata.get('location', '').lower()
|
| 1822 |
listing_type = ldata.get('listing_type', '')
|
| 1823 |
-
|
| 1824 |
# Simple keyword matching for demonstration
|
| 1825 |
if any(keyword in query_lower for keyword in [crop_type, location, listing_type, 'maize', 'beans', 'harare', 'bulawayo']):
|
| 1826 |
relevant_listings.append(ldata)
|
| 1827 |
-
|
| 1828 |
if not relevant_listings:
|
| 1829 |
return "No active listings found matching your query.", False
|
| 1830 |
-
|
| 1831 |
summary = "Active Listings:\n"
|
| 1832 |
for l in relevant_listings[:5]: # Limit to top 5 for brevity
|
| 1833 |
summary += f"- {l.get('crop_type')} ({l.get('listing_type')}) in {l.get('location')}, Qty: {l.get('quantity')}, Price: {l.get('asking_price') or l.get('price_range')}\n"
|
| 1834 |
-
|
| 1835 |
return summary, True
|
| 1836 |
|
| 1837 |
# Helper for AI chat to fetch price trends
|
|
@@ -1855,7 +1799,7 @@ def _get_price_trend_analysis_for_chat(crop_type=None, location=None):
|
|
| 1855 |
'date': deal.get('admin_approved_at') or deal.get('created_at'),
|
| 1856 |
'crop': deal_crop_type, 'location': deal_location
|
| 1857 |
})
|
| 1858 |
-
|
| 1859 |
if not price_data_points:
|
| 1860 |
return "Not enough historical data to generate price trends for the specified criteria.", False
|
| 1861 |
|
|
@@ -1885,12 +1829,12 @@ def get_price_trends():
|
|
| 1885 |
if not gemini_client: return jsonify({'error': 'AI service not available.'}), 503
|
| 1886 |
if not FIREBASE_INITIALIZED: return jsonify({'error': 'Server configuration error: Firebase not ready.'}), 503
|
| 1887 |
crop_type, location = request.args.get('crop_type'), request.args.get('location')
|
| 1888 |
-
|
| 1889 |
trend_analysis, success = _get_price_trend_analysis_for_chat(crop_type, location)
|
| 1890 |
-
|
| 1891 |
if not success:
|
| 1892 |
return jsonify({'message': trend_analysis}), 200 # trend_analysis contains the error message
|
| 1893 |
-
|
| 1894 |
return jsonify({'crop_type': crop_type, 'location': location, 'trend_analysis': trend_analysis}), 200
|
| 1895 |
except AttributeError as ae:
|
| 1896 |
logger.error(f"Gemini Response Attribute Error in get_price_trends: {ae}. Response object: {response_obj}")
|
|
@@ -2025,4 +1969,4 @@ def get_ai_chat_history():
|
|
| 2025 |
|
| 2026 |
if __name__ == '__main__':
|
| 2027 |
app.run(debug=True, host="0.0.0.0", port=int(os.getenv("PORT", 7860)))
|
| 2028 |
-
|
|
|
|
| 540 |
if field in data and data[field] != listing_data.get(field):
|
| 541 |
update_payload[field] = data[field]
|
| 542 |
requires_reapproval = True
|
| 543 |
+
|
| 544 |
# Allow user to change status to inactive/closed without re-approval
|
| 545 |
if 'status' in data and data['status'] in ['inactive', 'closed']:
|
| 546 |
update_payload['status'] = data['status']
|
|
|
|
| 550 |
update_payload['status'] = 'pending_approval'
|
| 551 |
else: # If no critical fields changed, allow status update if valid
|
| 552 |
update_payload['status'] = data['status'] # e.g., from inactive back to active if no critical changes
|
| 553 |
+
|
| 554 |
if requires_reapproval and update_payload.get('status') != 'pending_approval':
|
| 555 |
update_payload['status'] = 'pending_approval' # Ensure pending approval if critical fields changed
|
| 556 |
|
|
|
|
| 559 |
|
| 560 |
update_payload['last_updated_at'] = datetime.now(timezone.utc).isoformat()
|
| 561 |
listing_ref.update(update_payload)
|
| 562 |
+
|
| 563 |
message = 'Listing updated successfully.'
|
| 564 |
if requires_reapproval:
|
| 565 |
message += ' Listing status set to pending_approval due to significant changes.'
|
|
|
|
| 640 |
|
| 641 |
if not listing_data or not isinstance(listing_data, dict):
|
| 642 |
return jsonify({'error': 'Listing not found.'}), 404
|
| 643 |
+
|
| 644 |
lister_id = listing_data.get('lister_id')
|
| 645 |
listing_type = listing_data.get('listing_type', 'item')
|
| 646 |
crop_type = listing_data.get('crop_type', 'N/A')
|
| 647 |
|
| 648 |
listing_ref.delete()
|
| 649 |
+
|
| 650 |
if lister_id:
|
| 651 |
_send_system_notification(lister_id, f"Your {listing_type} listing for '{crop_type}' (ID: {listing_id}) has been removed by an administrator.", "listing_removed_by_admin", f"/my-listings")
|
| 652 |
|
|
|
|
| 660 |
@app.route('/api/deals/propose', methods=['POST'])
|
| 661 |
def propose_deal():
|
| 662 |
auth_header = request.headers.get('Authorization')
|
| 663 |
+
requester_uid = None # UID of the user making the API call (admin/user)
|
| 664 |
+
acting_uid = None # UID of the user on whose behalf the action is taken
|
| 665 |
+
|
| 666 |
try:
|
| 667 |
logger.info(f"Backend /api/deals/propose received headers: {request.headers}")
|
| 668 |
logger.info(f"Backend /api/deals/propose received raw data: {request.get_data(as_text=True)}")
|
| 669 |
|
| 670 |
+
requester_uid = verify_token(auth_header) # Can raise exceptions
|
| 671 |
if not FIREBASE_INITIALIZED:
|
| 672 |
return jsonify({'error': 'Server configuration error: Firebase not ready.'}), 503
|
| 673 |
|
|
|
|
| 676 |
logger.error("Backend /api/deals/propose: No JSON data received or failed to parse.")
|
| 677 |
return jsonify({'error': 'Invalid JSON data received.'}), 400
|
| 678 |
|
| 679 |
+
on_behalf_of_uid = data.get('on_behalf_of_uid')
|
| 680 |
+
|
| 681 |
+
if on_behalf_of_uid:
|
| 682 |
+
# If acting on behalf of someone, verify the requester is an admin/facilitator
|
| 683 |
+
admin_or_facilitator_uid, _, _ = verify_admin_or_facilitator(auth_header) # This will raise if not admin/facilitator
|
| 684 |
+
acting_uid = on_behalf_of_uid
|
| 685 |
+
|
| 686 |
+
# Verify the user on whose behalf we are acting exists and is a farmer (for WhatsApp farmers)
|
| 687 |
+
acting_user_profile = db.reference(f'users/{acting_uid}', app=db_app).get()
|
| 688 |
+
if not acting_user_profile:
|
| 689 |
+
return jsonify({'error': f'User {acting_uid} (on_behalf_of) not found.'}), 404
|
| 690 |
+
|
| 691 |
+
# Constraint: WhatsApp farmers can only list produce, so they can only *propose* against demand listings.
|
| 692 |
+
# This means the acting_uid must be a farmer.
|
| 693 |
+
if not acting_user_profile.get('roles', {}).get('farmer') and acting_user_profile.get('account_type') != 'whatsapp_managed':
|
| 694 |
+
return jsonify({'error': f'User {acting_uid} is not a farmer or WhatsApp-managed farmer, cannot propose on their behalf.'}), 403
|
| 695 |
+
|
| 696 |
+
logger.info(f"Admin/Facilitator {admin_or_facilitator_uid} proposing deal on behalf of {acting_uid}.")
|
| 697 |
+
else:
|
| 698 |
+
acting_uid = requester_uid # Regular user proposing
|
| 699 |
+
|
| 700 |
listing_id = data.get('listing_id')
|
|
|
|
| 701 |
proposed_quantity_by_user = data.get('quantity')
|
|
|
|
| 702 |
proposed_price_per_unit_by_user = data.get('price')
|
| 703 |
notes = data.get('notes', "")
|
| 704 |
|
|
|
|
| 727 |
original_lister_id = listing_data.get('lister_id')
|
| 728 |
listing_type = listing_data.get('listing_type') # 'produce' or 'demand'
|
| 729 |
|
| 730 |
+
if original_lister_id == acting_uid:
|
| 731 |
+
logger.error(f"Backend /api/deals/propose: User {acting_uid} attempting to deal with own listing {listing_id}.")
|
| 732 |
return jsonify({'error': 'Cannot propose a deal to your own listing/demand.'}), 400
|
| 733 |
|
| 734 |
deal_farmer_id = None
|
|
|
|
| 737 |
notification_recipient_id = original_lister_id # The other party
|
| 738 |
|
| 739 |
if listing_type == 'produce':
|
| 740 |
+
# Current user (proposer acting_uid) is a BUYER, proposing to a FARMER's produce listing.
|
| 741 |
+
# If acting on behalf of a WhatsApp farmer, they cannot be the buyer here.
|
| 742 |
+
if on_behalf_of_uid and acting_user_profile.get('account_type') == 'whatsapp_managed':
|
| 743 |
+
return jsonify({'error': 'WhatsApp farmers can only propose deals as a farmer (against demand listings).'}), 403
|
| 744 |
+
|
| 745 |
deal_farmer_id = original_lister_id
|
| 746 |
+
deal_buyer_id = acting_uid
|
| 747 |
deal_notes_prefix = "Buyer's proposal: "
|
| 748 |
|
| 749 |
available_quantity = listing_data.get('quantity', 0)
|
|
|
|
| 752 |
return jsonify({'error': f'Proposed quantity ({proposed_quantity_by_user}) exceeds available quantity ({available_quantity}) for the listing.'}), 400
|
| 753 |
|
| 754 |
elif listing_type == 'demand':
|
| 755 |
+
# Current user (proposer acting_uid) is a FARMER, making an offer against a BUYER's demand listing.
|
| 756 |
+
deal_farmer_id = acting_uid
|
| 757 |
deal_buyer_id = original_lister_id
|
| 758 |
deal_notes_prefix = "Farmer's offer against demand: "
|
| 759 |
|
|
|
|
| 769 |
deal_id = str(uuid.uuid4())
|
| 770 |
deal_data_to_set = {
|
| 771 |
'deal_id': deal_id,
|
| 772 |
+
'proposer_id': acting_uid, # Use acting_uid here
|
| 773 |
'listing_id': listing_id,
|
| 774 |
'farmer_id': deal_farmer_id,
|
| 775 |
'buyer_id': deal_buyer_id,
|
|
|
|
| 780 |
'created_at': datetime.now(timezone.utc).isoformat(),
|
| 781 |
'chat_room_id': f"deal_{deal_id}"
|
| 782 |
}
|
| 783 |
+
# If an admin acted on behalf, record that
|
| 784 |
+
if on_behalf_of_uid:
|
| 785 |
+
deal_data_to_set['proxied_by_admin_uid'] = requester_uid
|
| 786 |
+
|
| 787 |
db.reference(f'deals/{deal_id}', app=db_app).set(deal_data_to_set)
|
| 788 |
|
| 789 |
_send_system_notification(
|
| 790 |
notification_recipient_id,
|
| 791 |
+
f"You have a new proposal/offer from {acting_uid[:6]}... regarding your {listing_type} for '{listing_data.get('crop_type')}'. Qty: {proposed_quantity_by_user}, Price/Unit: {proposed_price_per_unit_by_user}",
|
| 792 |
"new_deal_proposal",
|
| 793 |
f"/deals/{deal_id}"
|
| 794 |
)
|
| 795 |
+
logger.info(f"Backend /api/deals/propose: Deal {deal_id} created successfully by UID {acting_uid} for listing {listing_id}.")
|
| 796 |
return jsonify({'success': True, 'message': 'Proposal/Offer submitted successfully.', 'deal': deal_data_to_set}), 201
|
| 797 |
|
| 798 |
except Exception as e:
|
| 799 |
# Log the original error before passing to generic handler
|
| 800 |
+
logger.error(f"Backend /api/deals/propose: Unhandled exception for UID {acting_uid or requester_uid or 'unknown'}. Error: {str(e)}\n{traceback.format_exc()}")
|
| 801 |
+
return handle_route_errors(e, uid_context=acting_uid or requester_uid or "propose_deal_unknown_user")
|
| 802 |
|
| 803 |
@app.route('/api/deals/<deal_id>/respond', methods=['POST'])
|
| 804 |
def respond_to_deal(deal_id):
|
| 805 |
auth_header = request.headers.get('Authorization')
|
| 806 |
+
requester_uid = None # UID of the user making the API call (admin/user)
|
| 807 |
+
acting_uid = None # UID of the user on whose behalf the action is taken
|
| 808 |
+
|
| 809 |
try:
|
| 810 |
+
requester_uid = verify_token(auth_header)
|
| 811 |
if not FIREBASE_INITIALIZED: return jsonify({'error': 'Server configuration error'}), 503
|
| 812 |
|
| 813 |
data = request.get_json()
|
| 814 |
response_action = data.get('action') # 'accept' or 'reject'
|
| 815 |
+
on_behalf_of_uid = data.get('on_behalf_of_uid')
|
| 816 |
+
|
| 817 |
+
if on_behalf_of_uid:
|
| 818 |
+
# If acting on behalf of someone, verify the requester is an admin/facilitator
|
| 819 |
+
admin_or_facilitator_uid, _, _ = verify_admin_or_facilitator(auth_header) # This will raise if not admin/facilitator
|
| 820 |
+
acting_uid = on_behalf_of_uid
|
| 821 |
+
|
| 822 |
+
# Verify the user on whose behalf we are acting exists and is a farmer (for WhatsApp farmers)
|
| 823 |
+
acting_user_profile = db.reference(f'users/{acting_uid}', app=db_app).get()
|
| 824 |
+
if not acting_user_profile:
|
| 825 |
+
return jsonify({'error': f'User {acting_uid} (on_behalf_of) not found.'}), 404
|
| 826 |
+
|
| 827 |
+
# Constraint: WhatsApp farmers can only list produce, so they would only *respond* as a farmer.
|
| 828 |
+
if not acting_user_profile.get('roles', {}).get('farmer') and acting_user_profile.get('account_type') != 'whatsapp_managed':
|
| 829 |
+
return jsonify({'error': f'User {acting_uid} is not a farmer or WhatsApp-managed farmer, cannot respond on their behalf.'}), 403
|
| 830 |
+
|
| 831 |
+
logger.info(f"Admin/Facilitator {admin_or_facilitator_uid} responding to deal {deal_id} on behalf of {acting_uid}.")
|
| 832 |
+
else:
|
| 833 |
+
acting_uid = requester_uid # Regular user responding
|
| 834 |
|
| 835 |
if response_action not in ['accept', 'reject']:
|
| 836 |
return jsonify({'error': 'Invalid action. Must be "accept" or "reject".'}), 400
|
|
|
|
| 849 |
# Authorization Check: Who is allowed to respond?
|
| 850 |
can_respond = False
|
| 851 |
if current_deal_status == 'proposed':
|
| 852 |
+
if proposer_id == buyer_id_in_deal and acting_uid == farmer_id_in_deal: # Buyer proposed, Farmer responds
|
| 853 |
can_respond = True
|
| 854 |
+
elif proposer_id == farmer_id_in_deal and acting_uid == buyer_id_in_deal: # Farmer proposed/countered, Buyer responds
|
| 855 |
can_respond = True
|
| 856 |
# Add other statuses if a different party needs to respond (e.g., after admin action)
|
| 857 |
|
| 858 |
if not can_respond:
|
| 859 |
+
logger.warning(f"UID {acting_uid} unauthorized to respond to deal {deal_id}. Deal status: {current_deal_status}, Proposer: {proposer_id}, Farmer: {farmer_id_in_deal}, Buyer: {buyer_id_in_deal}")
|
| 860 |
return jsonify({'error': 'Not authorized to respond to this deal at its current state.'}), 403
|
| 861 |
|
| 862 |
update_time = datetime.now(timezone.utc).isoformat()
|
| 863 |
|
| 864 |
# Determine the other party for notification
|
| 865 |
other_party_id = None
|
| 866 |
+
if acting_uid == farmer_id_in_deal:
|
| 867 |
other_party_id = buyer_id_in_deal
|
| 868 |
+
elif acting_uid == buyer_id_in_deal:
|
| 869 |
other_party_id = farmer_id_in_deal
|
| 870 |
|
| 871 |
listing_id = deal_data.get('listing_id')
|
|
|
|
| 874 |
listing_data_for_notif = db.reference(f'listings/{listing_id}', app=db_app).get() or {}
|
| 875 |
crop_type_for_notif = listing_data_for_notif.get('crop_type', 'your listing/demand')
|
| 876 |
|
| 877 |
+
update_payload = {}
|
| 878 |
+
if on_behalf_of_uid:
|
| 879 |
+
update_payload['proxied_by_admin_uid'] = requester_uid # Record who proxied the action
|
| 880 |
+
|
| 881 |
if response_action == 'accept':
|
| 882 |
# Quantity check (if applicable, e.g., if responding to a proposal against a produce listing)
|
| 883 |
+
if proposer_id == buyer_id_in_deal and acting_uid == farmer_id_in_deal: # Farmer accepting buyer's proposal
|
| 884 |
if listing_id and isinstance(listing_data_for_notif, dict):
|
| 885 |
available_quantity = listing_data_for_notif.get('quantity', 0)
|
| 886 |
proposed_quantity = deal_data.get('proposed_quantity', 0)
|
|
|
|
| 894 |
_send_system_notification(other_party_id, f"Your deal proposal for '{crop_type_for_notif}' could not be accepted due to insufficient stock.", "deal_status_update", f"/deals/{deal_id}")
|
| 895 |
return jsonify({'success': False, 'error': 'Deal could not be accepted. Listing quantity is no longer sufficient.'}), 409
|
| 896 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 897 |
accepted_by_field = ""
|
| 898 |
new_status = ""
|
| 899 |
+
if acting_uid == farmer_id_in_deal:
|
| 900 |
accepted_by_field = "farmer_accepted_at"
|
| 901 |
new_status = "accepted_by_farmer" # Farmer accepts buyer's proposal
|
| 902 |
notification_message_to_other_party = f"Your deal proposal for '{crop_type_for_notif}' has been ACCEPTED by the farmer. It is now pending admin approval."
|
| 903 |
+
elif acting_uid == buyer_id_in_deal:
|
| 904 |
accepted_by_field = "buyer_accepted_at"
|
| 905 |
new_status = "accepted_by_buyer" # Buyer accepts farmer's offer/counter
|
| 906 |
notification_message_to_other_party = f"Your offer/counter-offer for '{crop_type_for_notif}' has been ACCEPTED by the buyer. It is now pending admin approval."
|
| 907 |
else: # Should not happen due to auth check
|
| 908 |
return jsonify({'error': 'Internal error determining accepter role.'}), 500
|
| 909 |
|
| 910 |
+
update_payload.update({'status': new_status, accepted_by_field: update_time, 'last_responder_id': acting_uid})
|
| 911 |
+
deal_ref.update(update_payload)
|
| 912 |
|
| 913 |
if other_party_id:
|
| 914 |
_send_system_notification(other_party_id, notification_message_to_other_party, "deal_status_update", f"/deals/{deal_id}")
|
|
|
|
| 917 |
admins_ref = db.reference('users', app=db_app).order_by_child('is_admin').equal_to(True).get()
|
| 918 |
if admins_ref:
|
| 919 |
for admin_id_loop, _ in admins_ref.items():
|
| 920 |
+
_send_system_notification(admin_id_loop, f"Deal ID {deal_id} for '{crop_type_for_notif}' has been accepted by {acting_uid[:6]}... and needs your approval.", "admin_deal_approval_needed", f"/admin/deals/pending") # Path for admin to see pending deals
|
| 921 |
|
| 922 |
+
return jsonify({'success': True, 'message': f'Deal accepted by {("farmer" if acting_uid == farmer_id_in_deal else "buyer")}, pending admin approval.'}), 200
|
| 923 |
|
| 924 |
elif response_action == 'reject':
|
| 925 |
rejected_by_field = ""
|
| 926 |
new_status = ""
|
| 927 |
+
if acting_uid == farmer_id_in_deal:
|
| 928 |
rejected_by_field = "farmer_rejected_at" # Or just 'responded_at'
|
| 929 |
new_status = "rejected_by_farmer"
|
| 930 |
notification_message_to_other_party = f"Your deal proposal for '{crop_type_for_notif}' has been REJECTED by the farmer."
|
| 931 |
+
elif acting_uid == buyer_id_in_deal:
|
| 932 |
rejected_by_field = "buyer_rejected_at"
|
| 933 |
new_status = "rejected_by_buyer"
|
| 934 |
notification_message_to_other_party = f"Your offer/counter-offer for '{crop_type_for_notif}' has been REJECTED by the buyer."
|
| 935 |
else: # Should not happen
|
| 936 |
return jsonify({'error': 'Internal error determining rejector role.'}), 500
|
| 937 |
|
| 938 |
+
update_payload.update({'status': new_status, rejected_by_field: update_time, 'last_responder_id': acting_uid})
|
| 939 |
+
deal_ref.update(update_payload)
|
| 940 |
if other_party_id:
|
| 941 |
_send_system_notification(other_party_id, notification_message_to_other_party, "deal_status_update", f"/deals/{deal_id}")
|
| 942 |
+
return jsonify({'success': True, 'message': f'Deal rejected by {("farmer" if acting_uid == farmer_id_in_deal else "buyer")}.'}), 200
|
| 943 |
|
| 944 |
except Exception as e:
|
| 945 |
+
return handle_route_errors(e, uid_context=acting_uid or requester_uid)
|
| 946 |
|
| 947 |
@app.route('/api/deals/<deal_id>/complete', methods=['POST'])
|
| 948 |
def complete_deal_by_admin(deal_id):
|
|
|
|
| 1071 |
|
| 1072 |
if not deal_data or not isinstance(deal_data, dict):
|
| 1073 |
return jsonify({'error': 'Deal not found.'}), 404
|
| 1074 |
+
|
| 1075 |
farmer_id = deal_data.get('farmer_id')
|
| 1076 |
buyer_id = deal_data.get('buyer_id')
|
| 1077 |
transporter_id = deal_data.get('assigned_transporter_id')
|
| 1078 |
listing_id = deal_data.get('listing_id')
|
| 1079 |
+
|
| 1080 |
crop_type = "N/A"
|
| 1081 |
if listing_id:
|
| 1082 |
listing_details = db.reference(f'listings/{listing_id}', app=db_app).get()
|
|
|
|
| 1084 |
crop_type = listing_details.get('crop_type', 'N/A')
|
| 1085 |
|
| 1086 |
deal_ref.delete()
|
| 1087 |
+
|
| 1088 |
message_to_parties = f"Deal ID {deal_id} for '{crop_type}' has been removed by an administrator."
|
| 1089 |
if farmer_id:
|
| 1090 |
_send_system_notification(farmer_id, message_to_parties, "deal_removed_by_admin", f"/my-deals")
|
|
|
|
| 1098 |
except Exception as e:
|
| 1099 |
return handle_route_errors(e, uid_context=admin_uid)
|
| 1100 |
|
| 1101 |
+
# Removed the admin_create_manual_deal endpoint as its functionality is now integrated into propose_deal and respond_to_deal.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1102 |
|
| 1103 |
|
| 1104 |
#--- END OF MODIFIED DEAL MANAGEMENT SECTION ---
|
|
|
|
| 1752 |
# Helper for AI chat to fetch platform data
|
| 1753 |
def _fetch_platform_data_for_chat(query):
|
| 1754 |
if not FIREBASE_INITIALIZED: return "Firebase not ready.", False
|
| 1755 |
+
|
| 1756 |
listings_ref = db.reference('listings', app=db_app).order_by_child('status').equal_to('active')
|
| 1757 |
all_active_listings = listings_ref.get() or {}
|
| 1758 |
+
|
| 1759 |
relevant_listings = []
|
| 1760 |
query_lower = query.lower()
|
| 1761 |
+
|
| 1762 |
for lid, ldata in all_active_listings.items():
|
| 1763 |
if not ldata: continue
|
| 1764 |
crop_type = ldata.get('crop_type', '').lower()
|
| 1765 |
location = ldata.get('location', '').lower()
|
| 1766 |
listing_type = ldata.get('listing_type', '')
|
| 1767 |
+
|
| 1768 |
# Simple keyword matching for demonstration
|
| 1769 |
if any(keyword in query_lower for keyword in [crop_type, location, listing_type, 'maize', 'beans', 'harare', 'bulawayo']):
|
| 1770 |
relevant_listings.append(ldata)
|
| 1771 |
+
|
| 1772 |
if not relevant_listings:
|
| 1773 |
return "No active listings found matching your query.", False
|
| 1774 |
+
|
| 1775 |
summary = "Active Listings:\n"
|
| 1776 |
for l in relevant_listings[:5]: # Limit to top 5 for brevity
|
| 1777 |
summary += f"- {l.get('crop_type')} ({l.get('listing_type')}) in {l.get('location')}, Qty: {l.get('quantity')}, Price: {l.get('asking_price') or l.get('price_range')}\n"
|
| 1778 |
+
|
| 1779 |
return summary, True
|
| 1780 |
|
| 1781 |
# Helper for AI chat to fetch price trends
|
|
|
|
| 1799 |
'date': deal.get('admin_approved_at') or deal.get('created_at'),
|
| 1800 |
'crop': deal_crop_type, 'location': deal_location
|
| 1801 |
})
|
| 1802 |
+
|
| 1803 |
if not price_data_points:
|
| 1804 |
return "Not enough historical data to generate price trends for the specified criteria.", False
|
| 1805 |
|
|
|
|
| 1829 |
if not gemini_client: return jsonify({'error': 'AI service not available.'}), 503
|
| 1830 |
if not FIREBASE_INITIALIZED: return jsonify({'error': 'Server configuration error: Firebase not ready.'}), 503
|
| 1831 |
crop_type, location = request.args.get('crop_type'), request.args.get('location')
|
| 1832 |
+
|
| 1833 |
trend_analysis, success = _get_price_trend_analysis_for_chat(crop_type, location)
|
| 1834 |
+
|
| 1835 |
if not success:
|
| 1836 |
return jsonify({'message': trend_analysis}), 200 # trend_analysis contains the error message
|
| 1837 |
+
|
| 1838 |
return jsonify({'crop_type': crop_type, 'location': location, 'trend_analysis': trend_analysis}), 200
|
| 1839 |
except AttributeError as ae:
|
| 1840 |
logger.error(f"Gemini Response Attribute Error in get_price_trends: {ae}. Response object: {response_obj}")
|
|
|
|
| 1969 |
|
| 1970 |
if __name__ == '__main__':
|
| 1971 |
app.run(debug=True, host="0.0.0.0", port=int(os.getenv("PORT", 7860)))
|
| 1972 |
+
`
|