agmission/Development/server/controllers/partner.js

575 lines
18 KiB
JavaScript

'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<object>} 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=<customerId>
* OR GET /api/partners/aircraft?partnerId=<ObjectId>&customerId=<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
};