import logging import threading from datetime import datetime from flask import current_app from bson.objectid import ObjectId # Import the new REST API client helper from .xero_client import make_zoho_api_request from .extensions import mongo logger = logging.getLogger(__name__) def sync_user_approval_from_zoho(): """ Updates users' 'is_approved' status based on the 'cf_approval_status' custom dropdown in Zoho Books. A contact is considered approved when: custom_field.label == 'cf_approval_status' and custom_field.value == 'approved' This function pages through contacts and inspects custom_fields for the match. """ logger.info("Starting Zoho Books 'cf_approval_status' custom field sync.") try: page = 1 per_page = 200 # Zoho typically supports up to 200 per page; adjust if necessary approved_contact_ids = set() while True: params = {'page': page, 'per_page': per_page} response = make_zoho_api_request('GET', '/contacts', params=params) if not response: logger.warning(f"No response from Zoho when fetching contacts page {page}. Stopping pagination.") break logger.info(response) contacts = response[0]['contacts'] or [] if not contacts: logger.info(f"No contacts returned on page {page}. Pagination complete.") break for contact in contacts: # Contact id field might be 'contact_id' or similar - defensive access contact_id = contact.get('email') or contact.get('contactId') or contact.get('id') # Ensure we have a custom_fields list to check custom_fields = contact.get('custom_fields') or [] for cf in custom_fields: # cf typically has keys like 'label' and 'value' when you use label-based assignment if cf.get('label') == 'cf_approval_status' and str(cf.get('value')).lower() == 'approved': if contact_id: approved_contact_ids.add(str(contact_id)) break # no need to check other custom fields for this contact # If response contains page context we can use it; otherwise continue until an empty page page_context = response[0]['page_context'] has_more = page_context.get('has_more_page') if has_more is None: # fallback: stop when we received fewer than per_page results if len(contacts) < per_page: break page += 1 else: if not has_more: break page += 1 logger.info(f"Found {len(approved_contact_ids)} approved contacts in Zoho Books (cf_approval_status == 'approved').") # Set all users to not approved first (only users with a zoho_contact_id are considered) # mongo.db.users.update_many({'zoho_contact_id': {'$exists': True}}, {'$set': {'is_approved': False}}) # Then, set the ones found in the sync to approved if approved_contact_ids: mongo.db.users.update_many( {'email': {'$in': list(approved_contact_ids)}}, {'$set': {'is_approved': True}} ) logger.info(f"Zoho approval sync complete. {len(approved_contact_ids)} users are now marked as approved.") except Exception as e: logger.error(f"An error occurred during Zoho user approval sync: {e}", exc_info=True) def create_zoho_contact_async(app_context, registration_data): """ Creates a contact in Zoho Books, sets a 'pending' custom field, and adds a note (comment). Assumes a custom field exists whose label is 'cf_approval_status' (adjust label/index if needed). """ with app_context: user_email = registration_data.get('email') try: contact_person_name = registration_data.get('contactPerson', '') first_name, last_name = (contact_person_name.split(' ', 1) + [''])[:2] contact_payload = { 'contact_name': registration_data.get('businessName'), 'contact_type': 'customer', 'company_name': registration_data.get('companyName') , 'contact_persons': [{ 'first_name': first_name, 'last_name': last_name, 'email': user_email, 'phone': registration_data.get('phoneNumber'), # keep this: it's a valid flag for the contact person entry 'is_primary_contact': True }], 'billing_address': { 'address': registration_data.get('businessAddress', 'N/A') }, 'shipping_address': { 'address': registration_data.get('businessAddress', 'N/A') }, 'website': registration_data.get('companyWebsite'), # <-- Use label/index + value (not api_name) 'custom_fields': [ { 'label': 'cf_approval_status', # must match the field label in Zoho Books # 'index': 1, # optional: set if you know the slot (1..10) 'value': 'pending_for_approval' # value must match one of the dropdown option values } ] } response = make_zoho_api_request('POST', '/contacts', json_data=contact_payload) # defensive logging: log whole response so you can inspect Zoho's error message if any logger.debug(f"Zoho create contact response: {response}") # # basic success check — adjust depending on your make_zoho_api_request return structure # if not response or 'contact' not in response: # logger.error(f"Zoho Books contact creation failed for {user_email}: {response}") # return new_contact_id = response[0]['contact']['contact_id'] logger.info(f"Successfully created Zoho Books contact ({new_contact_id}) for user {user_email}.") mongo.db.users.update_one({'email': user_email}, {'$set': {'zoho_contact_id': new_contact_id}}) history_details = ( f"--- Client Application Details ---\n" f"Business Name: {registration_data.get('businessName')}\n" f"Contact Person: {registration_data.get('contactPerson')}\n" f"Email: {registration_data.get('email')}\n" f"Phone: {registration_data.get('phoneNumber')}\n" f"Company Website: {registration_data.get('companyWebsite')}\n" f"Business Address: {registration_data.get('businessAddress')}\n" f"Business Type: {registration_data.get('businessType')}\n" f"Years Operating: {registration_data.get('yearsOperating')}\n" f"Number of Locations: {registration_data.get('numLocations')}\n" f"Estimated Weekly Volume: {registration_data.get('estimatedVolume')}\n\n" f"--- Logistics Information ---\n" f"Preferred Delivery Times: {registration_data.get('preferredDeliveryTimes')}\n" f"Has Loading Dock: {registration_data.get('hasLoadingDock')}\n" f"Special Delivery Instructions: {registration_data.get('specialDeliveryInstructions')}\n" f"Minimum Quantity Requirements: {registration_data.get('minQuantityRequirements')}\n\n" f"--- Service & Billing ---\n" f"Reason for Switching: {registration_data.get('reasonForSwitch')}\n" f"Preferred Payment Method: {registration_data.get('paymentMethod')}\n\n" f"--- Additional Notes ---\n" f"{registration_data.get('additionalNotes')}" ) comment_payload = { 'description': history_details, 'show_comment_to_clients': False } make_zoho_api_request('POST', f'/contacts/{new_contact_id}/comments', json_data=comment_payload) logger.info(f"Successfully added registration details as a comment to Zoho contact {new_contact_id}.") except Exception as e: logger.error(f"Failed during Zoho Books contact/comment creation for user {user_email}. Error: {e}", exc_info=True) def trigger_contact_creation(registration_data): """Starts a background thread to create a Zoho contact.""" try: app_context = current_app.app_context() thread = threading.Thread(target=create_zoho_contact_async, args=(app_context, registration_data)) thread.daemon = True thread.start() logger.info(f"Started Zoho contact creation thread for {registration_data.get('email')}") except Exception as e: logger.error(f"Failed to start Zoho contact creation thread. Error: {e}") def get_zoho_contact_by_email(email): """Fetches a Zoho contact ID by email.""" try: # Assumes make_zoho_api_request is a helper function that handles authentication response = make_zoho_api_request('GET', '/contacts', params={'email': email}) contacts = response[0]['contacts'] if contacts: return contacts[0]['contact_id'] else: logger.info(f"No Zoho contact found for email: {email}") return None except Exception as e: logger.error(f"Error fetching Zoho contact for {email}: {e}", exc_info=True) return None def create_zoho_invoice_async(app_context, order_details): """Creates an Invoice in Zoho Books from order details (CAD currency, order no -> reference_number).""" with app_context: user_email = order_details['user_email'] contact_id = get_zoho_contact_by_email(user_email) if not contact_id: logger.error(f"Cannot create Invoice. Zoho contact not found for email {user_email}.") return line_items = [] for item in order_details['items']: product = mongo.db.products.find_one({'_id': ObjectId(item['productId'])}) for mode in product.get('modes', []): if str(mode) == item['mode']: product['zoho_id'] = product.get('modes')[mode].get('zoho_id') product['price'] = product.get('modes')[mode].get('price') break logger.info(f"Processing item {product.get('zoho_id')} with mode {item['mode']} for invoice creation.") unit = "lb" if item.get("mode") == "weight" else item.get("mode") line_items.append({ 'item_id': product['zoho_id'], 'quantity': int(item['quantity']), 'rate': float(product.get('price', 0)), 'description': f"{item['quantity']} {unit} of {product.get('name', 'N/A')}" }) if not line_items: logger.error("Zoho Invoice failed: No valid line items for order %s", order_details['order_id']) return # build invoice payload with CAD and order number logger.info( order_details.get('additional_info', 'N/A')) invoice_payload = { 'customer_id': contact_id, 'date': datetime.strptime(order_details.get("order_date", order_details["deliverydate"]), "%Y-%m-%d").strftime("%Y-%m-%d"), 'due_date': datetime.strptime(order_details["deliverydate"], "%Y-%m-%d").strftime("%Y-%m-%d"), 'line_items': line_items, 'notes': order_details.get('additional_info', 'N/A'), 'billing_address': { 'address': order_details.get('delivery_address') }, # <--- currency & order mapping 'currency_code': 'CAD', # invoice currency = Canadian Dollars 'exchange_rate': 1.0, # set to appropriate rate (1.0 if you treat amounts as CAD already) 'reference_number': order_details['order_id'], # shows your order number on the invoice # Optionally add custom fields if you have created one and know its customfield_id # 'custom_fields': [ # {'customfield_id': 123456789012345, 'value': order_details['order_id']} # ] } # send to Zoho response = make_zoho_api_request('POST', '/invoices', json_data=invoice_payload) print(response) invoice_id = response['invoice']['invoice_id'] logger.info(f"Successfully created Zoho Books Invoice {invoice_id} for order ID: {order_details['order_id']}") def trigger_invoice_creation(order_details): """Starts a background thread to create a Zoho Invoice.""" try: app_context = current_app.app_context() thread = threading.Thread(target=create_zoho_invoice_async, args=(app_context, order_details)) thread.daemon = True thread.start() logger.info(f"Started Zoho Invoice creation thread for order {order_details.get('order_id')}") except Exception as e: logger.error(f"Failed to start Zoho Invoice creation thread for order %s. Error: %s", order_details.get('order_id'), e)