diff --git a/VERSION b/VERSION index 022360f..c401c15 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.3.63 \ No newline at end of file +1.3.64 \ No newline at end of file diff --git a/app/contacts/backend/router_simple.py b/app/contacts/backend/router_simple.py index 002ee8c..bc5d038 100644 --- a/app/contacts/backend/router_simple.py +++ b/app/contacts/backend/router_simple.py @@ -101,7 +101,7 @@ async def get_contacts( {where_sql} GROUP BY c.id, c.first_name, c.last_name, c.email, c.phone, c.mobile, c.title, c.department, c.is_active, c.created_at, c.updated_at - ORDER BY c.first_name, c.last_name + ORDER BY company_count DESC, c.last_name, c.first_name LIMIT %s OFFSET %s """ params.extend([limit, offset]) diff --git a/app/system/backend/sync_router.py b/app/system/backend/sync_router.py index 56b286d..9fd1863 100644 --- a/app/system/backend/sync_router.py +++ b/app/system/backend/sync_router.py @@ -1,6 +1,33 @@ """ System Sync Router API endpoints for syncing data between vTiger, e-conomic and Hub + +SYNC ARCHITECTURE - Field Ownership: +===================================== + +E-CONOMIC owns and syncs: +- economic_customer_number (primary key from e-conomic) +- address, city, postal_code, country (physical address) +- email_domain, website (contact information) +- cvr_number (used for matching only, not overwritten if already set) + +vTIGER owns and syncs: +- vtiger_id (primary key from vTiger) +- vtiger_account_no +- Contact records and contact-company relationships + +HUB owns (manual or first-sync only): +- name (can be synced initially but not overwritten) +- cvr_number (used for matching, set once) +- Tags, notes, custom fields + +SYNC RULES: +=========== +1. NEVER overwrite source ID if already set (vtiger_id, economic_customer_number) +2. Match order: CVR → Source ID → Name (normalized) +3. Re-sync is idempotent - can run multiple times safely +4. Contact relationships are REPLACED on sync (not added) +5. Each sync only updates fields it owns """ import logging @@ -124,6 +151,7 @@ async def sync_from_vtiger() -> Dict[str, Any]: continue if current_vtiger_id is None: + # Only set vtiger_id if it's currently NULL execute_query( "UPDATE customers SET vtiger_id = %s, last_synced_at = NOW() WHERE id = %s", (vtiger_id, existing[0]['id']) @@ -131,13 +159,10 @@ async def sync_from_vtiger() -> Dict[str, Any]: linked_count += 1 logger.info(f"šŸ”— Linket: {existing[0]['name']} → vTiger #{vtiger_id} (match: {match_method}, CVR: {cvr or 'ingen'})") elif current_vtiger_id != vtiger_id: - # Update if different vTiger ID - execute_query( - "UPDATE customers SET vtiger_id = %s, last_synced_at = NOW() WHERE id = %s", - (vtiger_id, existing[0]['id']) - ) - updated_count += 1 - logger.info(f"āœļø Opdateret vTiger ID: {existing[0]['name']} → {vtiger_id} (var: {current_vtiger_id})") + # SKIP if different vTiger ID - do NOT overwrite existing vtiger_id + logger.warning(f"āš ļø Springer over: {existing[0]['name']} har allerede vTiger #{current_vtiger_id}, vil ikke overskrive med #{vtiger_id}") + not_found_count += 1 + continue else: # Already linked, just update timestamp execute_query("UPDATE customers SET last_synced_at = NOW() WHERE id = %s", (existing[0]['id'],)) @@ -386,19 +411,14 @@ async def sync_vtiger_contacts() -> Dict[str, Any]: customer_id = customer_result[0]['id'] customer_name = customer_result[0]['name'] - # Check if link already exists - existing_link = execute_query( - "SELECT id FROM contact_companies WHERE contact_id = %s AND customer_id = %s", - (contact_id, customer_id) + # DELETE existing links for this contact (we replace, not append) + # This ensures re-sync updates links to match current vTiger state + execute_query( + "DELETE FROM contact_companies WHERE contact_id = %s", + (contact_id,) ) - if existing_link: - # Already linked - if debug_count <= 20: - logger.warning(f" ↳ Already linked to '{customer_name}'") - continue - - # CREATE LINK + # CREATE new link from vTiger execute_query( "INSERT INTO contact_companies (contact_id, customer_id, is_primary) VALUES (%s, %s, false)", (contact_id, customer_id) @@ -502,12 +522,11 @@ async def sync_from_economic() -> Dict[str, Any]: ) if existing: - # Update existing customer (always sync economic_customer_number from e-conomic) + # Update existing customer - ONLY update fields e-conomic owns + # E-conomic does NOT overwrite: name, cvr_number (set once only) update_query = """ UPDATE customers SET - name = %s, economic_customer_number = %s, - cvr_number = %s, email_domain = %s, address = %s, city = %s, @@ -518,7 +537,7 @@ async def sync_from_economic() -> Dict[str, Any]: WHERE id = %s """ execute_query(update_query, ( - name, customer_number, cvr, email_domain, address, city, zip_code, country, website, existing[0]['id'] + customer_number, email_domain, address, city, zip_code, country, website, existing[0]['id'] )) updated_count += 1 logger.info(f"āœļø Opdateret: {name} (e-conomic #{customer_number}, CVR: {cvr or 'ingen'})") diff --git a/deploy_to_prod.sh b/deploy_to_prod.sh new file mode 100644 index 0000000..d5bc392 --- /dev/null +++ b/deploy_to_prod.sh @@ -0,0 +1,53 @@ +#!/bin/bash +# Deploy BMC Hub to production server +# Usage: ./deploy_to_prod.sh v1.3.56 + +set -e + +VERSION=$1 +PROD_SERVER="bmcadmin@172.16.31.183" +PROD_DIR="/srv/podman/bmc_hub_v1.0" + +if [ -z "$VERSION" ]; then + echo "āŒ Usage: ./deploy_to_prod.sh v1.3.56" + exit 1 +fi + +echo "šŸš€ Deploying $VERSION to production..." +echo "=======================================" + +# Update .env file +echo "šŸ“ Updating RELEASE_VERSION to $VERSION..." +ssh $PROD_SERVER "sed -i 's/^RELEASE_VERSION=.*/RELEASE_VERSION=$VERSION/' $PROD_DIR/.env" + +# Stop containers +echo "šŸ›‘ Stopping containers..." +ssh $PROD_SERVER "cd $PROD_DIR && podman-compose down" + +# Rebuild with no cache (to pull latest version from Gitea) +echo "šŸ”Ø Building new image from version $VERSION..." +ssh $PROD_SERVER "cd $PROD_DIR && podman-compose build --no-cache" + +# Start containers +echo "ā–¶ļø Starting containers..." +ssh $PROD_SERVER "cd $PROD_DIR && podman-compose up -d" + +# Wait for service to be ready +echo "ā³ Waiting for service to start..." +sleep 15 + +# Check health +echo "šŸ„ Checking service health..." +HEALTH=$(curl -s http://172.16.31.183:8000/health || echo "FAILED") +if echo "$HEALTH" | grep -q "ok"; then + echo "āœ… Deployment successful!" + echo "$HEALTH" +else + echo "āŒ Health check failed:" + echo "$HEALTH" + exit 1 +fi + +echo "" +echo "šŸŽ‰ Production is now running version $VERSION" +echo " http://172.16.31.183:8000" diff --git a/docker-compose.yml b/docker-compose.yml index b7a1b4e..80a958f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -42,6 +42,7 @@ services: # Mount for local development - live code reload - ./app:/app/app:ro - ./main.py:/app/main.py:ro + - ./VERSION:/app/VERSION:ro env_file: - .env environment: diff --git a/main.py b/main.py index fae7b6f..8a8d446 100644 --- a/main.py +++ b/main.py @@ -4,6 +4,7 @@ Main application entry point """ import logging +from pathlib import Path from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles @@ -13,6 +14,14 @@ from contextlib import asynccontextmanager from app.core.config import settings from app.core.database import init_db +def get_version(): + """Read version from VERSION file""" + try: + version_file = Path(__file__).parent / "VERSION" + return version_file.read_text().strip() + except Exception: + return "unknown" + # Import Feature Routers from app.customers.backend import router as customers_api from app.customers.backend import views as customers_views diff --git a/test_vtiger_accounts.py b/test_vtiger_accounts.py new file mode 100644 index 0000000..90dc2ca --- /dev/null +++ b/test_vtiger_accounts.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +import asyncio +import sys +sys.path.insert(0, '/Users/christianthomas/DEV/bmc_hub_dev') +from app.services.vtiger_service import VTigerService + +async def test_accounts(): + vtiger = VTigerService() + + # Check what 3x760 and 3x811 are in vTiger + for vtiger_id in ['3x760', '3x811']: + query = f"SELECT id, accountname FROM Accounts WHERE id = '{vtiger_id}';" + result = await vtiger.query(query) + + if result: + print(f"{vtiger_id} = {result[0].get('accountname')}") + else: + print(f"{vtiger_id} = NOT FOUND") + +asyncio.run(test_accounts()) diff --git a/test_vtiger_contact.py b/test_vtiger_contact.py new file mode 100644 index 0000000..90ec995 --- /dev/null +++ b/test_vtiger_contact.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +""" +Test script to check vTiger contact data +""" +import asyncio +import sys +sys.path.insert(0, '/Users/christianthomas/DEV/bmc_hub_dev') + +from app.services.vtiger_service import VTigerService + +async def test_contact(): + vtiger = VTigerService() + + # Fetch contact by email + query = "SELECT id, firstname, lastname, email, account_id FROM Contacts WHERE email = 'accounting@arbodania.com';" + print(f"Query: {query}") + + result = await vtiger.query(query) + + if result: + contact = result[0] + print(f"\nāœ… Contact found:") + print(f" ID: {contact.get('id')}") + print(f" Name: {contact.get('firstname')} {contact.get('lastname')}") + print(f" Email: {contact.get('email')}") + print(f" Account ID: {contact.get('account_id')}") + + # Now fetch the account details + account_id = contact.get('account_id') + if account_id: + account_query = f"SELECT id, accountname FROM Accounts WHERE id = '{account_id}';" + print(f"\nFetching account: {account_query}") + account_result = await vtiger.query(account_query) + + if account_result: + account = account_result[0] + print(f"\nāœ… Account found:") + print(f" ID: {account.get('id')}") + print(f" Name: {account.get('accountname')}") + else: + print("āŒ Contact not found") + +if __name__ == "__main__": + asyncio.run(test_contact())