'use strict'; const env = require('../helpers/env'); const { Partner, PartnerSystemUser } = require('../model/partner'), User = require('../model/user'), { AppParamError, AppError, AppAuthError } = require('../helpers/app_error'), { Errors, UserTypes, PartnerCodes } = require('../helpers/constants'), { updateUser_put } = require('./user'), // Import user controller functions, partnerSyncService = require('../services/partner_sync_service'), ObjectId = require('mongodb').ObjectId, assert = require('assert'); /** * Helper function to find and validate a partner system user * @param {string} customerId - Customer ID * @param {string} partnerId - Partner ID (can be partnerCode string or ObjectId) * @param {object} options - Additional validation options * @param {boolean} options.requireActive - Whether to require active status (default: true) * @param {string} options.username - Username for credential validation * @param {string} options.password - Password for credential validation * @returns {Promise} Partner system user with populated partner and customer info */ async function findAndValidatePartnerSystemUser(customerId, partnerId, options = {}) { const { requireActive = true, username, password } = options; // Validate IDs if (!ObjectId.isValid(customerId)) { throw new AppParamError('Invalid customerId format'); } let query = { parent: ObjectId(customerId) }; // Use parent field for customer lookup // Handle partnerId - could be ObjectId or partner code string if (ObjectId.isValid(partnerId)) { query.partner = ObjectId(partnerId); } else { // If it's a string like 'SATLOC', find partner by partnerCode const partner = await Partner.findOne({ partnerCode: partnerId }).lean(); if (!partner) { throw new AppParamError(Errors.NOT_FOUND, `Partner not found with code: ${partnerId}`); } query.partner = partner._id; } // If username provided, include it in the lookup query if (username) query.username = username; // Find the partner system user const partnerSystemUser = await PartnerSystemUser.findOne(query) .populate('partner', 'name partnerCode') .populate('parent', 'name username'); // parent = customer relationship if (!partnerSystemUser) { throw new AppParamError(Errors.NOT_FOUND, `Partner system user not found for customerId: ${customerId} and partnerId: ${partnerId}`); } if (requireActive && !partnerSystemUser.active) { throw new AppParamError(Errors.INACTIVE, 'Partner system user is inactive'); } // Validate password if provided (username was already matched via query) if (password && String(partnerSystemUser.password).localeCompare(String(password)) !== 0) { throw new AppAuthError(Errors.WRONG_CREDENTIAL); } return partnerSystemUser; } // Create a new partner organization async function createPartner_post(req, res) { const _partner = req.body; assert(_partner && _partner.name, AppParamError.create()); delete _partner._id; const partner = new Partner(Partner.toUser(_partner)); const newPartner = await partner.save(); res.json(newPartner); } // Create a new partner system user (RESTful POST /systemUsers) async function createSystemUser_post(req, res) { const input = req.body; assert(input && input.partnerId && input.customerId && input.username && input.password, AppParamError.create()); // Validate both partnerId and customerId exist in one query const parentUsers = await User.find({ _id: { $in: [ObjectId(input.partnerId), ObjectId(input.customerId)] } }); if (parentUsers.length < 2) AppParamError.throw(); const partnerSystemUser = new PartnerSystemUser({ kind: UserTypes.PARTNER_SYSTEM_USER, name: input.name || `${input.username} (Partner System)`, username: input.username, password: input.password, active: input.active !== undefined ? input.active : true, // Parent relationship - links to the applicator (customer) user parent: input.customerId, // This is the customer/applicator reference // Partner system user fields partner: input.partnerId, // NOTE: 'customer' field removed - use 'parent' for customer relationship // partnerUserId: input.partnerUserId, // partnerUsername: input.partnerUsername, companyId: input?.companyId, apiKey: input?.apiKey, apiSecret: input?.apiSecret, metadata: input?.metadata }); const newPartnerSystemUser = await partnerSystemUser.save(); res.json(newPartnerSystemUser); } // Get all partners async function getPartners_post(req, res) { const partners = await Partner.find().lean(); res.json(partners); } // Get partner by ID async function getPartnerById_post(req, res) { const { partner } = req.body; assert(partner, AppParamError.create()); const partnerData = await Partner.findById(partner).lean(); if (!partnerData) AppParamError.throw(`Partner not found with ID: ${partner}`); if (!partnerData) AppParamError.throw(Errors.NOT_FOUND, `Partner not found with ID: ${partner}`); res.json(partnerData); } // Update a partner (RESTful PUT /partners/:id) - reuses user controller logic async function updatePartner_put(req, res) { if (!req.body.kind) { req.body.kind = UserTypes.PARTNER; } // Reuse the user controller's updateUser_put function directly return updateUser_put(req, res); } // Update a partner system user (only updates fields present in request body) async function updateSystemUser_post(req, res) { const systemUserId = req.params.id; const input = req.body; assert(systemUserId, AppParamError.create()); // Build update object with only fields present in input const updateFields = {}; const allowedFields = [ 'name', 'username', 'active', 'partner', 'parent', // parent = customer relationship 'partnerUserId', 'partnerUsername', 'companyId', 'apiKey', 'apiSecret', 'metadata' ]; // Map 'customer' to 'parent' for backward compatibility if (input.hasOwnProperty('customer') && !input.hasOwnProperty('parent')) { input.parent = input.customer; } allowedFields.forEach(field => { if (input.hasOwnProperty(field)) { updateFields[field] = input[field]; } }); let partnerSystemUser; // If no fields to update, just return the current record if (Object.keys(updateFields).length === 0) { partnerSystemUser = await PartnerSystemUser.findById(systemUserId).lean(); } else { partnerSystemUser = await PartnerSystemUser.findByIdAndUpdate( systemUserId, { $set: updateFields }, { new: true } ).lean(); } if (!partnerSystemUser) AppParamError.throw(Errors.NOT_FOUND); res.json(partnerSystemUser); } // Delete a partner (soft delete - mark as inactive) async function deletePartner(req, res) { const { id } = req.params; assert(id, AppParamError.create()); const partner = await Partner.findByIdAndUpdate( id, { active: false }, { new: true } ).lean(); if (!partner) AppParamError.throw(Errors.NOT_FOUND); res.json({ ok: true }); } // Sync data from partner system async function syncData_post(req, res) { try { const { partnerCode, customer } = req.body; assert(partnerCode && customer, AppParamError.create()); // Implementation depends on specific partner integration // This is a placeholder for partner sync functionality res.json({ success: true, message: 'Partner data sync initiated', partnerCode, customer }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } } // Upload job to partner system async function uploadJob_post(req, res) { try { const { partnerCode, customer, jobData } = req.body; assert(partnerCode && customer && jobData, AppParamError.create()); // Implementation depends on specific partner integration // This is a placeholder for partner job upload functionality res.json({ success: true, message: 'Job upload to partner system initiated', partnerCode, customer, jobId: jobData.jobId }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } } async function getPartners_get(req, res) { const partners = await Partner.find({ markedDelete: { $ne: true } }).lean(); res.json(partners); } async function getPartnerById_get(req, res) { const { id } = req.params; assert(id, AppParamError.create()); const partner = await Partner.findById(id).lean(); if (!partner) AppParamError.throw(Errors.NOT_FOUND, `Partner not found with ID: ${id}`); res.json(partner); } async function getSystemUsers_get(req, res) { const { partnerId, customerId } = req.query; // Validate mandatory parameters assert(ObjectId.isValid(partnerId) && ObjectId.isValid(customerId), AppParamError.create()); const query = { partner: partnerId, parent: customerId // Use parent field for customer lookup }; const partnerSystemUsers = await PartnerSystemUser.find(query) .populate({ path: 'partner', select: 'name partnerCode' }) .populate({ path: 'parent', select: 'name username' }) // parent = customer .lean(); res.json(partnerSystemUsers); } // Get current (first active) system user for a partner+customer pair async function getCurrentSystemUser_get(req, res) { const { partnerId, customerId } = req.query; assert(ObjectId.isValid(partnerId) && ObjectId.isValid(customerId), AppParamError.create()); const partnerSystemUser = await PartnerSystemUser.findOne({ partner: ObjectId(partnerId), parent: ObjectId(customerId), active: true, markedDelete: { $ne: true } }) .populate({ path: 'partner', select: 'name partnerCode' }) .populate({ path: 'parent', select: 'name username' }) .lean(); if (!partnerSystemUser) AppParamError.throw(Errors.NOT_FOUND); res.json(partnerSystemUser); } // Get system user by ID (RESTful GET /systemUsers/:id) async function getSystemUser_get(req, res) { const { id } = req.params; assert(id, AppParamError.create()); const partnerSystemUser = await PartnerSystemUser.findById(id) .populate('partner', 'name partnerCode') .populate('parent', 'name username') // parent = customer .lean(); if (!partnerSystemUser) AppParamError.throw(Errors.NOT_FOUND); res.json(partnerSystemUser); } // Update system user (RESTful PUT /systemUsers/:id) - reuses user controller logic async function updateSystemUser_put(req, res) { if (!req.body.kind) { req.body.kind = UserTypes.PARTNER_SYSTEM_USER; } // Reuse the user controller's updateUser_put function directly return updateUser_put(req, res); } // Delete system user (soft delete - mark as inactive) async function deleteSystemUser(req, res) { const { id } = req.params; assert(id, AppParamError.create()); const partnerSystemUser = await PartnerSystemUser.findByIdAndUpdate( id, { active: false }, { new: true } ).lean(); if (!partnerSystemUser) AppParamError.throw(Errors.NOT_FOUND); res.json({ ok: true }); } // Get all customers for a given partner with subscription information async function getPartnerCustomers_get(req, res) { const { partnerId } = req.query; if (!partnerId) AppParamError.throw(); // Aggregate customers linked to the partner through partner system users const customers = await PartnerSystemUser.aggregate([ // Match partner system users for the given partner { $match: { partner: ObjectId(partnerId), active: true, markedDelete: { $ne: true } } }, // Lookup the customer information (using parent field) { $lookup: { from: 'users', localField: 'parent', // parent = customer relationship foreignField: '_id', as: 'customerInfo' } }, // Unwind customer info { $unwind: '$customerInfo' }, // Filter only active customers { $match: { 'customerInfo.active': true, 'customerInfo.markedDelete': { $ne: true } } }, // Project the fields we want to return { $project: { _id: '$customerInfo._id', name: '$customerInfo.name', email: '$customerInfo.email', username: '$customerInfo.username', contact: '$customerInfo.contact', active: '$customerInfo.active', country: '$customerInfo.country', createdAt: '$customerInfo.createdAt', updatedAt: '$customerInfo.updatedAt', // Temporarily include membership for processing 'membership.subscriptions': '$customerInfo.membership.subscriptions' } }, // Add computed fields for package information { $addFields: { packageInfo: { $map: { input: { $filter: { input: { $ifNull: ['$membership.subscriptions', []] }, cond: { $eq: ['$$this.type', 'package'] } } }, as: 'sub', in: { packageName: { $arrayElemAt: ['$$sub.items.price', 0] // Get the first (and usually only) price/lookup_key }, status: '$$sub.status', startDate: { $dateFromParts: { year: { $year: { $toDate: { $multiply: ['$$sub.periodStart', 1000] } } }, month: { $month: { $toDate: { $multiply: ['$$sub.periodStart', 1000] } } }, day: { $dayOfMonth: { $toDate: { $multiply: ['$$sub.periodStart', 1000] } } } } }, endDate: { $dateFromParts: { year: { $year: { $toDate: { $multiply: ['$$sub.periodEnd', 1000] } } }, month: { $month: { $toDate: { $multiply: ['$$sub.periodEnd', 1000] } } }, day: { $dayOfMonth: { $toDate: { $multiply: ['$$sub.periodEnd', 1000] } } } } }, recurring: '$$sub.recurring' } } } } }, // Remove the membership field from final output { $project: { membership: 0 } }, // Sort by customer name { $sort: { name: 1 } } ]); res.json(customers); } // Test authentication/login of a partner system user async function testPartnerAuth_post(req, res) { const { customerId, partnerId, username, password } = req.body; if (!customerId || !partnerId || !username || !password) { AppParamError.throw(Errors.INVALID_PARAM); } // Use helper function to find and validate partner system user const partnerSystemUser = await findAndValidatePartnerSystemUser(customerId, partnerId, { requireActive: true, username, password }); // Try to get partner service and test authentication const partnerCode = partnerSystemUser.partner.partnerCode; const partnerServiceFactory = require('../services/partner_service_factory'); // Check if partner service is supported if (!partnerServiceFactory.hasService(partnerCode)) { AppParamError.throw(Errors.NOT_SUPPORTED, `Partner service not supported for code: ${partnerCode}`); } // Get the partner service dynamically const partnerService = partnerServiceFactory.getService(partnerCode); const partnerConfig = require('../helpers/partner_config'); // Get credentials for authentication const credentials = partnerConfig.getApiCredentials(partnerSystemUser, partnerCode); // Use authenticate() without caching for testing purposes const authResult = await partnerService.authenticate({ username, password }, customerId); res.json({ ok: true, ...!env.PRODUCTION ? {} : { authResult } // Include authResult only in non-production for debugging }); } /** * Get aircraft list from partner system * GET /api/partners/aircraft?partnerId=SATLOC&customerId= * OR GET /api/partners/aircraft?partnerId=&customerId= */ async function getPartnerAircraft_get(req, res) { const { partnerId, customerId } = req.query; // Validate required parameters if (!partnerId || !ObjectId.isValid(customerId)) AppParamError.throw(); try { let partnerCode; // Handle partnerId - could be ObjectId or partner code string if (ObjectId.isValid(partnerId)) { // If it's an ObjectId, find partner by ID to get partnerCode const partner = await Partner.findById(partnerId).lean(); if (!partner) { return res.status(422).json({ success: false, partnerId, customerId, error: `Partner not found with ID: ${partnerId}` }); } partnerCode = partner.partnerCode; } else { // If it's a string like 'SATLOC', use it directly as partnerCode partnerCode = partnerId; } // Get aircraft list from partner service using partnerCode const result = await partnerSyncService.getPartnerAircraftList(partnerCode, customerId); if (result.success) { res.json({ success: true, partnerId, customerId, partnerCode, aircraft: result.aircraft || [] }); } else { // Don't hardcode 400 - let service errors bubble up or return appropriate status res.status(422).json({ success: false, partnerId, customerId, partnerCode, error: result.error }); } } catch (error) { // Let AppParamError and other app errors bubble up to error handler middleware if (error instanceof AppParamError || error instanceof AppError) { throw error; } // Handle unexpected errors res.status(500).json({ success: false, partnerId, customerId, error: error.message }); } } module.exports = { createPartner_post, createSystemUser_post, getPartners_post, getPartners_get, getSystemUsers_get, getPartnerById_post, getPartnerById_get, updatePartner_put, updateSystemUser_post, deletePartner, syncData_post, uploadJob_post, getCurrentSystemUser_get, getSystemUser_get, updateSystemUser_put, deleteSystemUser, getPartnerCustomers_get, testPartnerAuth_post, getPartnerAircraft_get };