'use strict'; const logger = require('../helpers/logger'); const pino = logger.child('partner_sync_service'); const { AppParamError, AppError } = require('../helpers/app_error'); /** * Simple Partner Sync Service * Handles synchronization of jobs with partner systems using environment-based configuration */ const { HealthStatus, PartnerOperations, AssignStatus, Errors } = require('../helpers/constants'), partnerServiceFactory = require('./partner_service_factory'), { JobStatus } = require('../helpers/job_constants'), { runWithSessionOrTransaction } = require('../helpers/mongo_enhanced'), JobAssign = require('../model/job_assign'), jobUtil = require('../helpers/job_util'); class PartnerSyncService { constructor() { this.activeServices = new Map(); this.initializeServices(); } initializeServices() { // Initialize all supported partner services const supportedPartners = partnerServiceFactory.getSupportedPartners(); supportedPartners.forEach(partnerCode => { try { const service = partnerServiceFactory.getService(partnerCode); this.activeServices.set(partnerCode, service); pino.info(`Partner service initialized: ${partnerCode}`); } catch (error) { pino.warn({ err: error }, `Failed to initialize partner service: ${partnerCode}`); } }); } /** * Upload job data to partner system * @param {string} assignId - Job assignment ID * @param {object} options - Options object * @param {object} options.session - MongoDB session for atomic transactions * @returns {Promise} Upload result */ async uploadJobToPartner(assignId, options = {}) { try { const assignments = await JobAssign.findByIdWithPartnerInfo(assignId); const assignment = Array.isArray(assignments) ? assignments[0] : assignments; if (!assignment) { logger.logError(new Error('Assignment not found'), { operation: PartnerOperations.UPLOAD_JOB, assignmentId: assignId }); AppParamError.throw(Errors.INVALID_ASSIGNMENT); } // Check if user (vehicle) has partner integration if (!assignment.hasPartnerIntegration()) { return { success: false, message: 'No partner integration, no upload needed' }; } const partnerCode = assignment.getPartnerCode(); const partnerService = this.activeServices.get(partnerCode); if (!partnerService) { logger.logError(new Error('Partner service not available'), { operation: PartnerOperations.UPLOAD_JOB, assignmentId: assignId, partnerCode }); AppError.throw(Errors.PARTNER_SERVICE_UNAVAILABLE); } // Let the partner service decide how to format the job data const result = await partnerService.uploadJobDataToAircraft(assignment); logger.logPartnerOperation( PartnerOperations.UPLOAD_JOB, partnerCode, result.success, { assignmentId: assignId, jobId: assignment.job._id, partnerAircraftId: assignment.getPartnerAircraftId(), partnerJobId: result.externalJobId } ); // If upload was successful, update assignment status and write job log atomically if (result.success) { // Use runWithSessionOrTransaction to properly handle both provided sessions and new transactions await runWithSessionOrTransaction(async (session) => { // Update assignment status to 'uploaded' await jobUtil.updateAssignStatusById(assignId, AssignStatus.UPLOADED, { externalJobId: result.externalJobId, date: new Date(), }, session); // Write Uploaded job log entry, job status to Downloaded await jobUtil.writeJobLog(assignment.job._id, AssignStatus.UPLOADED, assignment.user._id, { updateJobStatus: true, jobStatusValue: JobStatus.DOWNLOADED, session: session }); }, options.session); } return { success: result.success, message: result.message, externalJobId: result.externalJobId, partnerCode, partnerAircraftId: assignment.getPartnerAircraftId() }; } catch (error) { logger.logError(error, { operation: PartnerOperations.UPLOAD_JOB, assignmentId: assignId }); throw error; } } /** * Check health of all partner services * @returns {Promise} Health status */ async healthCheck() { const results = { overall: HealthStatus.HEALTHY, partners: {} }; for (const [partnerCode, service] of this.activeServices) { try { // Use the service's healthCheck method which calls /IsAlive for SatLoc const health = await service.healthCheck(); results.partners[partnerCode] = { status: health.isAlive ? HealthStatus.HEALTHY : HealthStatus.UNHEALTHY, isAlive: health.isAlive, timestamp: health.timestamp, responseTime: health.responseTime, error: health.error }; } catch (error) { results.partners[partnerCode] = { status: HealthStatus.UNHEALTHY, isAlive: false, error: error.message, timestamp: new Date().toISOString() }; results.overall = HealthStatus.DEGRADED; } } if (Object.values(results.partners).some(p => p.status === HealthStatus.UNHEALTHY)) { results.overall = HealthStatus.UNHEALTHY; } return results; } /** * Get available partner services * @returns {array} Available partner codes */ getAvailablePartners() { return Array.from(this.activeServices.keys()); } /** * Check if a partner service is available * @param {string} partnerCode - Partner code, must be uppercase or it will be converted to Uppercase while looking it up * @returns {boolean} Whether service is available */ isPartnerAvailable(partnerCode) { return this.activeServices.has(String(partnerCode).toUpperCase()); } /** * Check health of a specific partner API * @param {string} partnerCode - Partner code to check * @returns {Promise} Whether API is live */ async checkPartnerAPIHealth(partnerCode) { try { if (!this.isPartnerAvailable(partnerCode)) { return false; } // Quick health check for the specific partner const partnerService = this.activeServices.get(partnerCode); if (!partnerService || !partnerService.healthCheck) { return false; } const healthResult = await Promise.race([ partnerService.healthCheck(), new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 5000)) ]); return healthResult && healthResult.healthy !== false; } catch (error) { pino.debug({ err: error }, `Partner API health check failed for ${partnerCode}`); return false; } } /** * Get aircraft list from a partner system * @param {string} partnerCode - Partner code (e.g., 'SATLOC') * @param {string} customerId - Customer ID * @returns {Promise} Aircraft list response */ async getPartnerAircraftList(partnerCode, customerId) { try { if (!this.isPartnerAvailable(partnerCode)) { return { success: false, error: `Invalid partner code: ${partnerCode}`, partnerCode }; } const partnerService = this.activeServices.get(partnerCode); if (!partnerService || typeof partnerService.getAircraftList !== 'function') { return { success: false, error: `Aircraft list method not available for partner: ${partnerCode}`, partnerCode }; } // Call partner-specific aircraft list method const result = await partnerService.getAircraftList(customerId); logger.logPartnerOperation( PartnerOperations.GET_AIRCRAFT_LIST, partnerCode, result.success, { customerId, aircraftCount: result.aircraft?.length || 0 } ); return result; } catch (error) { logger.logError(error, { operation: PartnerOperations.GET_AIRCRAFT_LIST, partnerCode, customerId }); return { success: false, error: error.message, partnerCode }; } } } module.exports = new PartnerSyncService();