import { BadRequestException, forwardRef, Inject, Injectable, NotFoundException } from '@nestjs/common'; import { ListMasterItemsRepository } from '../repository/list-master-items.repository'; import { ListMasterRepository } from '../repository/list-master.repository'; import { ApiRegistryService } from '../../third-party-module/service/api-registry.service'; import { firstValueFrom } from 'rxjs'; import { HttpService } from '@nestjs/axios'; import { EntityManager } from 'typeorm'; import { EntityMasterService } from 'src/module/meta/service/entity-master.service'; import { UserData } from 'src/module/user/entity/user.entity'; import { EntityServiceImpl } from 'src/module/meta/service/entity-service-impl.service'; import { STATUS_ACTIVE, STATUS_INACTIVE } from 'src/constant/global.constant'; import { Action } from '../../workflow-automation/interface/action.interface'; import axios from 'axios'; import { ConfigService } from '@nestjs/config'; import { AttributeMasterService } from '../../meta/service/attribute-master.service'; import { IMicroserviceClients } from '../../microservice-client/service/microservice-clients'; @Injectable() export class ListMasterService { constructor( private readonly entityManager: EntityManager, @Inject(forwardRef(() => EntityServiceImpl)) private readonly entityServiceImpl: EntityServiceImpl, @Inject(forwardRef(() => EntityMasterService)) private readonly entityMasterService: EntityMasterService, private readonly listMasterRepo: ListMasterRepository, private readonly listItemsRepo: ListMasterItemsRepository, private readonly apiRegistryService: ApiRegistryService, private readonly httpService: HttpService, private readonly configService: ConfigService, private readonly attributeMasterService: AttributeMasterService, @Inject('MICROSERVICE_CLIENT_FACTORY') private readonly factory: IMicroserviceClients, ) { } schema = this.configService.get('DB_SCHEMA'); private readonly skipLevelFilterEntities = ['BRN', 'USR', 'UPR', 'BRNP']; private readonly actions = new Map(); registerAction(actionName: string, actionInstance: Action) { this.actions.set(actionName, actionInstance); console.log( `⚙️ [WorkflowAutomationEngine] Registered action: ${actionName}`, ); } async getResolvedListCode( code: string, enterprise_id: number, ): Promise { if (!code) return code; // Check if it's a valid list type const listMaster = await this.listItemsRepo.findItemByCode( code, enterprise_id, ); if (listMaster) { return listMaster; } } async getDropdownOptions( type: string, params: Record, inactiveIdsArray?: number[], loggedInUser?, publicCall = false, ) { const config = await this.listMasterRepo.findByType( type, loggedInUser?.enterprise_id, ); if (!config) throw new NotFoundException(`Type ${type} not found`); if ( config.appcode != null && config.appcode != loggedInUser?.appcode && !publicCall ) { // Call internal API for appcode mismatch try { const baseUrl = this.configService.get('REDIRECT_BE_URL'); // Prepare the query string const queryParams = new URLSearchParams({ inactiveIds: inactiveIdsArray?.join(',') || '', loggedInUser: JSON.stringify(loggedInUser), ...params, // Spread other params into the query string }).toString(); // Make the GET request with query parameters const response = await axios.get( `${baseUrl}/list-master/getDropdownDataPublic/${type}?${queryParams}`, { headers: { 'Content-Type': 'application/json', }, }, ); return response.data; } catch (error) { console.error('⚠️ Internal API call failed:', error.message); throw new BadRequestException( `Failed to fetch dropdown for type ${type} from internal API`, ); } } // Fallback to old logic if no extension exists switch (config.source) { case 'entity': return this.fetchFromEntity( type, params, inactiveIdsArray, loggedInUser, ); case 'master': return this.listItemsRepo.findItemsByType( type, config.sort_by, inactiveIdsArray, loggedInUser?.enterprise_id, params, ); case 'operator': return this.listItemsRepo.findOperatorsByType( type, loggedInUser?.enterprise_id, ); case 'custom': // If you want Axios call here too: try { const response = await axios.get( `https://external-source.com/${config.custom_source_id}`, { params }, ); return response.data; } catch (error) { console.error('⚠️ Custom source fetch failed:', error.message); throw new BadRequestException('Failed to fetch custom source'); } default: throw new BadRequestException(`Unknown source: ${config.source}`); } } private async fetchFromEntity( sourceList: string, params: Record, inactiveIdsArray?: number[], loggedInUser?: UserData, ) { let result: { label: string; value: number }[] = []; if (!sourceList) return result; const entityMaster = await this.entityMasterService.getEntityData( sourceList, loggedInUser, ); const tableName = entityMaster.data_source; const applyCommonFilters = (qb: any, status?: number) => { if (status) { const isView = tableName.endsWith('_vw'); // auto-detect view const statusColumn = isView ? 'status_id' : 'status'; qb.andWhere(`${tableName}.${statusColumn} = :status`, { status }); } if (loggedInUser?.level_type && loggedInUser?.level_id) { // Skip level filter for certain entities if (!this.skipLevelFilterEntities.includes(sourceList)) { qb.andWhere( `${tableName}.level_type = :levelType AND ${tableName}.level_id = :levelId AND ${tableName}.enterprise_id = :enterprise_id`, { levelType: loggedInUser.level_type, levelId: loggedInUser.level_id, enterprise_id: loggedInUser.enterprise_id, }, ); } } if (sourceList == 'BRN') { // IN the case of BRN, we don't filter by level qb.andWhere( `${tableName}.parent_id = :enterprise_id AND ${tableName}.type = 'BRN'`, { enterprise_id: loggedInUser?.enterprise_id }, ); } if (sourceList == 'BRNP') { // IN the case of BRNP, we only filter by organization_id qb.andWhere(`${tableName}.enterprise_id = :enterprise_id`, { enterprise_id: loggedInUser?.enterprise_id, }); } if (sourceList == 'USR' || sourceList == 'UPR') { // IN the case of USR/UPR, we don't filter by level qb.andWhere(`${tableName}.enterprise_id = :enterprise_id`, { enterprise_id: loggedInUser?.enterprise_id, }); } if (loggedInUser?.appcode && sourceList === 'ROL') { if (!this.skipLevelFilterEntities.includes(sourceList)) { qb.andWhere(`${tableName}.appcode = :appcode`, { appcode: loggedInUser.appcode, }); } } // ✅ Apply dynamic params for (const key in params) { qb.andWhere(`${tableName}.${key} = :${key}`, { [key]: params[key], }); } // ✅ New: Exclude customers for USR/UPR if (sourceList === 'USR' || sourceList === 'UPR') { qb.andWhere(`${tableName}.is_customer is NULL`); } return qb; }; const resolveStatus = await this.getResolvedListCode( STATUS_ACTIVE, loggedInUser?.enterprise_id || 0, ); // Fetch active records const activeQuery = applyCommonFilters( this.entityManager .createQueryBuilder() .select('*') .from(`${this.schema}.${tableName}`, tableName), resolveStatus.id, ); const activeResults = await activeQuery.getRawMany(); const activeIds = new Set(activeResults.map((r) => r.id)); // Add active entries first activeResults.forEach((r) => { result.push({ label: r.name, value: r.id }); }); const resolveInactiveStatus = await this.getResolvedListCode( STATUS_INACTIVE, loggedInUser?.organization_id || 0, ); // Fetch inactive records (with same filters but without status condition) if (inactiveIdsArray?.length) { const inactiveQuery = applyCommonFilters( this.entityManager .createQueryBuilder() .select('*') .from(`${this.schema}.${tableName}`, tableName), resolveInactiveStatus.id, ); inactiveQuery.andWhere(`${tableName}.id IN (:...ids)`, { ids: inactiveIdsArray, }); const inactiveResults = await inactiveQuery.getRawMany(); inactiveResults.forEach((item) => { if (!activeIds.has(item.id)) { result.push({ label: `${item.name} [INACTIVE]`, value: item.id, }); } }); } return result; } private async fetchFromExternalSource( customSourceId: number, params?: Record, ) { const apiRegistry = await this.apiRegistryService.findById(customSourceId); if (!apiRegistry) return []; const url = `${apiRegistry.base_url}${apiRegistry.endpoint.replace(/{{(.*?)}}/g, (_, key) => params?.[key.trim()] || '')}`; const payload = this.injectDynamicParams( apiRegistry.request_payload_schema, params, ); const response = await firstValueFrom( this.httpService.request({ url, method: apiRegistry.http_method || 'POST', data: payload, headers: apiRegistry.headers ? apiRegistry.headers : undefined, }), ); const data = this.extractByPath( response.data, apiRegistry.response_data_path, ); if (!Array.isArray(data)) { return [{ label: data, value: data }]; } if (typeof data[0] === 'string') { return data.map((item: string) => ({ label: item, value: item })); } return data.map((item: any) => ({ label: item[apiRegistry?.label], value: item[apiRegistry?.value], })); } private injectDynamicParams( payload: any, params?: Record, ): any { if (params) { const stringified = JSON.stringify(payload); const replaced = stringified.replace( /{{(.*?)}}/g, (_, key) => params[key.trim()] || '', ); return JSON.parse(replaced); } } private extractByPath(obj: any, path: string): any { if (!path || !obj) return obj; return path.split('.').reduce((acc, key) => { if (acc == null) return undefined; // Try to convert to number if it's a numeric string (for array indexes) const index = Number(key); if (!isNaN(index) && Array.isArray(acc)) { return acc[index]; } return acc[key]; }, obj); } // createEntity method async createEntity(entityData: any, loggedInUser: UserData): Promise { // Trim and validate name and code const name = entityData.name?.trim(); // const code = entityData.code?.trim(); if (!name) { throw new BadRequestException('Name is required and cannot be empty'); } entityData.name = name; entityData.is_factory = entityData.is_factory || 0; entityData.source = entityData.source || 'master'; entityData.status = entityData.status || 'ACTIVE'; entityData.type = entityData.code; entityData.sort_by = entityData.sort_by || 'asc'; // Check for duplicate name const nameExists = await this.listMasterRepo.findOneByCondition({ name, enterprise_id: loggedInUser.enterprise_id, }); if (nameExists) { throw new BadRequestException( 'A List Master with the same name already exists', ); } const createdListMaster = await this.entityServiceImpl.createEntity( entityData, loggedInUser, ); if (!createdListMaster) { throw new BadRequestException('Failed to create entity'); } return createdListMaster; } async updateEntity(entityData: any, loggedInUser: UserData): Promise { return await this.entityServiceImpl.updateEntity(entityData, loggedInUser); } async getEntityData( entity_type: string, id: number, loggedInUser, ): Promise { return await this.entityServiceImpl.getEntityData( entity_type, id, loggedInUser, ); } async getAllListMasterByOrganization( enterprise_id: number, search?: string, ): Promise { return await this.listMasterRepo.findAllItems(enterprise_id, search); } async getDropDownData( entity_type: string, attribute_key: string, loggedInUser: UserData, body: Record, ) { let entityMaster = await this.entityMasterService.getEntityData( entity_type, loggedInUser, ); let appCode = entityMaster.appcode; const { inactiveIds, ...params } = body; const inactiveIdsArray = inactiveIds ? inactiveIds.split(',').map((id) => parseInt(id, 10)) : []; const currentAppCode = this.configService.get('appcode'); if (currentAppCode === appCode || !currentAppCode) { const entityAttribute = await this.attributeMasterService.findByMappedEntityTypeAndAttributeKey( entity_type, attribute_key, loggedInUser, ); if (entityAttribute && entityAttribute.data_source_type) { const listMaster = await this.listMasterRepo.findByType( entityAttribute.datasource_list, loggedInUser?.enterprise_id, ); if (!listMaster) { return; } switch (listMaster.source) { case 'entity': return this.fetchFromEntity( listMaster.type, params, inactiveIdsArray, loggedInUser, ); case 'master': return this.listItemsRepo.findItemsByType( listMaster.type, listMaster.sort_by, inactiveIdsArray, loggedInUser.enterprise_id, params, ); case 'operator': return this.listItemsRepo.findOperatorsByType( listMaster?.type, loggedInUser?.enterprise_id, ); case 'custom': // If you want Axios call here too: try { const response = await axios.get( `https://external-source.com/${listMaster.custom_source_id}`, { params }, ); return response.data; } catch (error) { console.error('⚠️ Custom source fetch failed:', error.message); throw new BadRequestException('Failed to fetch custom source'); } default: throw new BadRequestException( `Unknown source: ${listMaster.source}`, ); } } } else { const client = this.factory.getClient(appCode); if (!client) { return; } return client .send('getDropdownData', { entity_type, attribute_key, loggedInUser, body, }) .toPromise(); } } }