agmission/Development/server/services/partner_sync_service.js

268 lines
8.3 KiB
JavaScript

'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<object>} 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<object>} 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<boolean>} 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<object>} 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();