// External sync service with Google and Microsoft contacts integration // Types and clients extracted to separate modules use crate::contacts::sync_types::*; use crate::contacts::google_client::GoogleClient; use crate::contacts::microsoft_client::MicrosoftClient; use chrono::{DateTime, Utc}; use log::{debug, error, warn}; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; use uuid::Uuid; use crate::core::shared::state::AppState; // External contact types - now in sync_types.rs // Google/Microsoft clients - now in google_client.rs and microsoft_client.rs #[derive(Debug, Clone)] pub struct GoogleConfig { pub client_id: String, pub client_secret: String, } #[derive(Debug, Clone)] pub struct MicrosoftConfig { pub client_id: String, pub client_secret: String, pub tenant_id: String, } pub struct ExternalSyncService { google_client: GoogleClient, microsoft_client: MicrosoftClient, accounts: Arc>>, mappings: Arc>>, sync_history: Arc>>, contacts: Arc>>, } impl ExternalSyncService { pub fn new(google_config: GoogleConfig, microsoft_config: MicrosoftConfig) -> Self { Self { google_client: GoogleClient::new(), microsoft_client: MicrosoftClient::new(), accounts: Arc::new(RwLock::new(HashMap::new())), mappings: Arc::new(RwLock::new(HashMap::new())), sync_history: Arc::new(RwLock::new(Vec::new())), contacts: Arc::new(RwLock::new(HashMap::new())), } } // Keep main sync methods - account management, sync operations, conflict resolution async fn find_existing_account( &self, organization_id: Uuid, provider: &ExternalProvider, external_id: &str, ) -> Result, ExternalSyncError> { let accounts = self.accounts.read().await; Ok(accounts.values().find(|a| { a.organization_id == organization_id && &a.provider == provider && a.external_account_id == external_id }).cloned()) } async fn update_account_tokens( &self, account_id: Uuid, tokens: &TokenResponse, ) -> Result { let mut accounts = self.accounts.write().await; let account = accounts.get_mut(&account_id) .ok_or_else(|| ExternalSyncError::DatabaseError("Account not found".into()))?; account.access_token = tokens.access_token.clone(); account.refresh_token = tokens.refresh_token.clone(); account.token_expires_at = tokens.expires_at; account.updated_at = Utc::now(); Ok(account.clone()) } async fn save_account(&self, account: &ExternalAccount) -> Result<(), ExternalSyncError> { let mut accounts = self.accounts.write().await; accounts.insert(account.id, account.clone()); Ok(()) } async fn get_account(&self, account_id: Uuid) -> Result { let accounts = self.accounts.read().await; accounts.get(&account_id).cloned() .ok_or_else(|| ExternalSyncError::DatabaseError("Account not found".into())) } pub async fn connect_account( &self, organization_id: Uuid, user_id: Uuid, request: &ConnectAccountRequest, ) -> Result { // Exchange authorization code for tokens let tokens = match request.provider { ExternalProvider::Google => { self.google_client .exchange_code(&request.authorization_code, &request.redirect_uri) .await? } ExternalProvider::Microsoft => { self.microsoft_client .exchange_code(&request.authorization_code, &request.redirect_uri) .await? } _ => { return Err(ExternalSyncError::UnsupportedProvider(request.provider.to_string())) } }; // Get user info from provider let user_info = match request.provider { ExternalProvider::Google => { self.google_client.get_user_info(&tokens.access_token).await? } ExternalProvider::Microsoft => { self.microsoft_client.get_user_info(&tokens.access_token).await? } _ => return Err(ExternalSyncError::UnsupportedProvider(request.provider.to_string())), }; // Check if account already exists if let Some(existing) = self .find_existing_account(organization_id, &request.provider, &user_info.id) .await? { // Update tokens return self.update_account_tokens(existing.id, &tokens).await; } // Create new account let account_id = Uuid::new_v4(); let now = Utc::now(); let account = ExternalAccount { id: account_id, organization_id, user_id, provider: request.provider.clone(), external_account_id: user_info.id, email: user_info.email, display_name: user_info.name, access_token: tokens.access_token, refresh_token: tokens.refresh_token, token_expires_at: tokens.expires_at, scopes: tokens.scopes, sync_enabled: true, sync_direction: request.sync_direction.clone().unwrap_or_default(), last_sync_at: None, last_sync_status: None, sync_cursor: None, created_at: now, updated_at: now, }; self.save_account(&account).await?; Ok(account) } pub async fn disconnect_account( &self, organization_id: Uuid, account_id: Uuid, ) -> Result<(), ExternalSyncError> { let account = self.get_account(account_id).await?; if account.organization_id != organization_id { return Err(ExternalSyncError::Unauthorized); } // Revoke tokens with provider match account.provider { ExternalProvider::Google => { let _ = self.google_client.revoke_token(&account.access_token).await; } ExternalProvider::Microsoft => { let _ = self.microsoft_client.revoke_token(&account.access_token).await; } _ => {} } // Delete account and mappings self.delete_account(account_id).await?; Ok(()) } pub async fn start_sync( &self, organization_id: Uuid, account_id: Uuid, request: &StartSyncRequest, trigger: SyncTrigger, ) -> Result { let account = self.get_account(account_id).await?; if account.organization_id != organization_id { return Err(ExternalSyncError::Unauthorized); } if !account.sync_enabled { return Err(ExternalSyncError::SyncDisabled); } // Check if sync already in progress if let Some(last_status) = &account.last_sync_status { if last_status == "in_progress" { return Err(ExternalSyncError::SyncInProgress); } } let sync_id = Uuid::new_v4(); let now = Utc::now(); let direction = request.direction.clone().unwrap_or(account.sync_direction); let mut history = SyncHistory { id: sync_id, account_id, started_at: now, completed_at: None, status: SyncStatus::InProgress, direction: direction.clone(), contacts_created: 0, contacts_updated: 0, contacts_deleted: 0, contacts_skipped: 0, conflicts_detected: 0, errors: vec![], triggered_by: trigger, }; self.save_sync_history(&history).await?; // Perform sync based on direction let result = match direction { SyncDirection::TwoWay => { self.perform_two_way_sync(&account, request.full_sync.unwrap_or(false), &mut history) .await } SyncDirection::ImportOnly => { self.perform_import_sync(&account, request.full_sync.unwrap_or(false), &mut history) .await } SyncDirection::ExportOnly => { self.perform_export_sync(&account, &mut history).await } }; // Update history with results history.completed_at = Some(Utc::now()); history.status = if result.is_ok() { if history.errors.is_empty() { SyncStatus::Success } else { SyncStatus::PartialSuccess } } else { SyncStatus::Failed }; self.save_sync_history(&history).await?; self.update_account_sync_status(account_id, history.status.clone()) .await?; if let Err(e) = result { return Err(e); } Ok(history) } async fn perform_two_way_sync( &self, account: &ExternalAccount, full_sync: bool, history: &mut SyncHistory, ) -> Result<(), ExternalSyncError> { // First import from external self.perform_import_sync(account, full_sync, history).await?; // Then export to external self.perform_export_sync(account, history).await?; Ok(()) } async fn perform_import_sync( &self, account: &ExternalAccount, full_sync: bool, history: &mut SyncHistory, ) -> Result<(), ExternalSyncError> { let sync_cursor = if full_sync { None } else { account.sync_cursor.clone() }; // Fetch contacts from provider let (external_contacts, new_cursor) = match account.provider { ExternalProvider::Google => { self.google_client.fetch_contacts(&account.access_token).await? } ExternalProvider::Microsoft => { self.microsoft_client.fetch_contacts(&account.access_token).await? } _ => return Err(ExternalSyncError::UnsupportedProvider(account.provider.to_string())), }; // Update sync cursor self.update_account_sync_cursor(account.id, new_cursor).await?; // Process each contact for external_contact in external_contacts { match self.import_contact(account, &external_contact, history).await { Ok(ImportResult::Created) => history.contacts_created += 1, Ok(ImportResult::Updated) => history.contacts_updated += 1, Ok(ImportResult::Skipped) => history.contacts_skipped += 1, Ok(ImportResult::Conflict) => history.conflicts_detected += 1, Err(e) => { history.errors.push(SyncError { contact_id: None, external_id: Some(external_contact.id.clone()), operation: "import".to_string(), error_code: "import_failed".to_string(), error_message: e.to_string(), retryable: true, }); } } } Ok(()) } async fn perform_export_sync( &self, account: &ExternalAccount, history: &mut SyncHistory, ) -> Result<(), ExternalSyncError> { // Get pending uploads let pending_contacts = self.get_pending_uploads(account.id).await?; for mapping in pending_contacts { match self.export_contact(account, &mapping, history).await { Ok(ExportResult::Created) => history.contacts_created += 1, Ok(ExportResult::Updated) => history.contacts_updated += 1, Ok(ExportResult::Deleted) => history.contacts_deleted += 1, Ok(ExportResult::Skipped) => history.contacts_skipped += 1, Err(e) => { history.errors.push(SyncError { contact_id: Some(mapping.local_contact_id), external_id: Some(mapping.external_contact_id.clone()), operation: "export".to_string(), error_code: "export_failed".to_string(), error_message: e.to_string(), retryable: true, }); } } } Ok(()) } async fn import_contact( &self, account: &ExternalAccount, external: &ExternalContact, history: &mut SyncHistory, ) -> Result { let existing_mapping = self .get_mapping_by_external_id(account.id, &external.id) .await?; if let Some(mapping) = existing_mapping { // Check for conflicts let internal_changed = self.has_internal_changes(&mapping).await?; if internal_changed { return Ok(ImportResult::Conflict); } self.update_mapping_after_sync(mapping.id, external.etag).await?; return Ok(ImportResult::Updated); } // Create new mapping and internal contact let contact_id = self.create_internal_contact(account.organization_id, external).await?; let now = Utc::now(); let new_mapping = ContactMapping { id: Uuid::new_v4(), account_id: account.id, contact_id, local_contact_id: contact_id, external_id: external.id.clone(), external_contact_id: external.id.clone(), external_etag: external.etag.clone(), internal_version: 1, last_synced_at: now, sync_status: MappingSyncStatus::Synced, conflict_data: None, local_data: None, remote_data: None, conflict_detected_at: None, created_at: now, updated_at: now, }; self.create_mapping(&new_mapping).await?; Ok(ImportResult::Created) } async fn export_contact( &self, account: &ExternalAccount, mapping: &ContactMapping, history: &mut SyncHistory, ) -> Result { let internal = self.get_internal_contact(mapping.local_contact_id).await?; let external = self.convert_to_external(&internal).await?; match account.provider { ExternalProvider::Google => { self.google_client .update_contact(&account.access_token, &mapping.external_contact_id, &external) .await?; } ExternalProvider::Microsoft => { self.microsoft_client .update_contact(&account.access_token, &mapping.external_contact_id, &external) .await?; } _ => return Err(ExternalSyncError::UnsupportedProvider(account.provider.to_string())), } self.update_mapping_after_sync(mapping.id, external.etag).await?; Ok(ExportResult::Updated) } async fn resolve_conflict( &self, organization_id: Uuid, mapping_id: Uuid, request: &ResolveConflictRequest, ) -> Result { let mapping = self.get_mapping(mapping_id).await?; let account = self.get_account(mapping.account_id).await?; if account.organization_id != organization_id { return Err(ExternalSyncError::Unauthorized); } let resolved_contact = match request.resolution { ConflictResolution::KeepInternal => { mapping.local_data.clone() } ConflictResolution::KeepExternal => { mapping.remote_data.clone() } ConflictResolution::KeepLocal => { mapping.local_data.clone() } ConflictResolution::KeepRemote => { mapping.remote_data.clone() } ConflictResolution::Merge => { let mut merged = mapping.local_data.clone().unwrap_or_default(); if let Some(remote) = &mapping.remote_data { merged = remote.clone(); } request.merged_data.as_ref().map(|m| { merged.first_name = m.first_name.clone().or(merged.first_name); merged.last_name = m.last_name.clone().or(merged.last_name); merged.email = m.email.clone().or(merged.email); merged.phone = m.phone.clone().or(merged.phone); merged.company = m.company.clone().or(merged.company); merged.notes = m.notes.clone().or(merged.notes); }); Some(merged) } ConflictResolution::Manual => { request.manual_data.clone() } ConflictResolution::Skip => { return Ok(mapping.clone()); } }; let now = Utc::now(); let updated_mapping = ContactMapping { id: mapping.id, account_id: mapping.account_id, contact_id: mapping.contact_id, local_contact_id: mapping.local_contact_id, external_id: mapping.external_id.clone(), external_contact_id: mapping.external_contact_id.clone(), external_etag: mapping.external_etag.clone(), internal_version: mapping.internal_version + 1, last_synced_at: now, sync_status: MappingSyncStatus::Synced, conflict_data: None, local_data: resolved_contact.clone(), remote_data: mapping.remote_data.clone(), conflict_detected_at: None, created_at: mapping.created_at, updated_at: now, }; let mut mappings = self.mappings.write().await; mappings.insert(updated_mapping.id, updated_mapping.clone()); Ok(updated_mapping) } // Helper methods async fn create_internal_contact( &self, _organization_id: Uuid, external: &ExternalContact, ) -> Result { let contact_id = Uuid::new_v4(); let mut contacts = self.contacts.write().await; let mut contact = external.clone(); contact.id = contact_id.to_string(); contacts.insert(contact_id, contact); Ok(contact_id) } async fn get_internal_contact(&self, contact_id: Uuid) -> Result { let contacts = self.contacts.read().await; contacts.get(&contact_id).cloned() .ok_or_else(|| ExternalSyncError::DatabaseError("Contact not found".into())) } async fn convert_to_external(&self, contact: &ExternalContact) -> Result { Ok(contact.clone()) } async fn has_internal_changes(&self, _mapping: &ContactMapping) -> Result { Ok(false) } async fn create_mapping(&self, mapping: &ContactMapping) -> Result<(), ExternalSyncError> { let mut mappings = self.mappings.write().await; mappings.insert(mapping.id, mapping.clone()); Ok(()) } async fn save_sync_history(&self, history: &SyncHistory) -> Result<(), ExternalSyncError> { let mut sync_history = self.sync_history.write().await; sync_history.push(history.clone()); Ok(()) } async fn update_account_sync_status( &self, account_id: Uuid, status: SyncStatus, ) -> Result<(), ExternalSyncError> { let mut accounts = self.accounts.write().await; if let Some(account) = accounts.get_mut(&account_id) { account.last_sync_status = Some(status.to_string()); account.last_sync_at = Some(Utc::now()); } Ok(()) } async fn update_account_sync_cursor( &self, account_id: Uuid, cursor: Option, ) -> Result<(), ExternalSyncError> { let mut accounts = self.accounts.write().await; if let Some(account) = accounts.get_mut(&account_id) { account.sync_cursor = cursor; } Ok(()) } async fn get_pending_uploads(&self, account_id: Uuid) -> Result, ExternalSyncError> { let mappings = self.mappings.read().await; Ok(mappings.values() .filter(|m| m.account_id == account_id && m.sync_status == MappingSyncStatus::PendingUpload) .cloned() .collect()) } async fn get_mapping_by_external_id( &self, account_id: Uuid, external_id: &str, ) -> Result, ExternalSyncError> { let mappings = self.mappings.read().await; Ok(mappings.values() .find(|m| m.account_id == account_id && m.external_id == external_id) .cloned()) } async fn get_mapping(&self, mapping_id: Uuid) -> Result { let mappings = self.mappings.read().await; mappings.get(&mapping_id).cloned() .ok_or_else(|| ExternalSyncError::DatabaseError("Mapping not found".into())) } } // Error type - now uses types from sync_types #[derive(Debug, Clone)] pub enum ExternalSyncError { DatabaseError(String), UnsupportedProvider(String), Unauthorized, SyncDisabled, SyncInProgress, ApiError(String), InvalidData(String), NetworkError(String), AuthError(String), ParseError(String), } impl std::fmt::Display for ExternalSyncError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::DatabaseError(e) => write!(f, "Database error: {e}"), Self::UnsupportedProvider(p) => write!(f, "Unsupported provider: {p}"), Self::Unauthorized => write!(f, "Unauthorized"), Self::SyncDisabled => write!(f, "Sync is disabled"), Self::SyncInProgress => write!(f, "Sync already in progress"), Self::ApiError(e) => write!(f, "API error: {e}"), Self::InvalidData(e) => write!(f, "Invalid data: {e}"), Self::NetworkError(e) => write!(f, "Network error: {e}"), Self::AuthError(e) => write!(f, "Auth error: {e}"), Self::ParseError(e) => write!(f, "Parse error: {e}"), } } } impl std::error::Error for ExternalSyncError {} impl axum::response::IntoResponse for ExternalSyncError { fn into_response(self) -> axum::response::Response { let status = match self { Self::DatabaseError(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::Unauthorized => StatusCode::UNAUTHORIZED, Self::UnsupportedProvider(_) => StatusCode::BAD_REQUEST, Self::SyncDisabled => StatusCode::FORBIDDEN, Self::SyncInProgress => StatusCode::CONFLICT, Self::InvalidData(_) => StatusCode::BAD_REQUEST, Self::ApiError(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::NetworkError(_) => StatusCode::SERVICE_UNAVAILABLE, Self::AuthError(_) => StatusCode::UNAUTHORIZED, Self::ParseError(_) => StatusCode::INTERNAL_SERVER_ERROR, }; (status, self.to_string()).into_response() } } // External contact and related types #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExternalContact { pub id: String, pub etag: Option, pub first_name: Option, pub last_name: Option, pub display_name: Option, pub email_addresses: Vec, pub phone_numbers: Vec, pub addresses: Vec, pub company: Option, pub job_title: Option, pub department: Option, pub notes: Option, pub birthday: Option, pub photo_url: Option, pub groups: Vec, pub custom_fields: HashMap, pub created_at: Option>, pub updated_at: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExternalEmail { pub address: String, pub label: Option, pub primary: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExternalPhone { pub number: String, pub label: Option, pub primary: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExternalAddress { pub street: Option, pub city: Option, pub state: Option, pub postal_code: Option, pub country: Option, pub label: Option, pub primary: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UserEmail { pub email: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UserPhone { pub phone: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UserName { pub name: String, }