From ee361a0c4ba611c87efe7f97d044a7c711024d1b Mon Sep 17 00:00:00 2001 From: laxmanhalaki Date: Fri, 12 Dec 2025 21:39:52 +0530 Subject: [PATCH] reporting manager is picked using display name while creating claim request --- src/controllers/dealerClaim.controller.ts | 118 ++++++++-- src/controllers/user.controller.ts | 33 +++ src/models/WorkflowRequest.ts | 4 +- src/routes/dealerClaim.routes.ts | 9 +- src/routes/user.routes.ts | 3 + src/services/dealerClaim.service.ts | 268 ++++++++++++++++------ src/services/user.service.ts | 40 ++++ 7 files changed, 388 insertions(+), 87 deletions(-) diff --git a/src/controllers/dealerClaim.controller.ts b/src/controllers/dealerClaim.controller.ts index 5da5e45..9a46a55 100644 --- a/src/controllers/dealerClaim.controller.ts +++ b/src/controllers/dealerClaim.controller.ts @@ -6,6 +6,7 @@ import logger from '../utils/logger'; import { gcsStorageService } from '../services/gcsStorage.service'; import { Document } from '../models/Document'; import { constants } from '../config/constants'; +import { sapIntegrationService } from '../services/sapIntegration.service'; import fs from 'fs'; import path from 'path'; import crypto from 'crypto'; @@ -38,6 +39,7 @@ export class DealerClaimController { periodStartDate, periodEndDate, estimatedBudget, + selectedManagerEmail, // Optional: When multiple managers found, user selects one } = req.body; // Validation @@ -59,13 +61,46 @@ export class DealerClaimController { periodStartDate: periodStartDate ? new Date(periodStartDate) : undefined, periodEndDate: periodEndDate ? new Date(periodEndDate) : undefined, estimatedBudget: estimatedBudget ? parseFloat(estimatedBudget) : undefined, + selectedManagerEmail, // Pass selected manager email if provided }); return ResponseHandler.success(res, { request: claimRequest, message: 'Claim request created successfully' }, 'Claim request created'); - } catch (error) { + } catch (error: any) { + // Handle multiple managers found error + if (error.code === 'MULTIPLE_MANAGERS_FOUND') { + const response: any = { + success: false, + message: 'Multiple reporting managers found. Please select one.', + error: { + code: 'MULTIPLE_MANAGERS_FOUND', + managers: error.managers || [] + }, + timestamp: new Date(), + }; + logger.warn('[DealerClaimController] Multiple managers found:', { managers: error.managers }); + res.status(400).json(response); + return; + } + + // Handle no manager found error + if (error.code === 'NO_MANAGER_FOUND') { + const response: any = { + success: false, + message: error.message || 'No reporting manager found. Please ensure your manager is correctly configured in the system.', + error: { + code: 'NO_MANAGER_FOUND', + message: error.message + }, + timestamp: new Date(), + }; + logger.warn('[DealerClaimController] No manager found:', { message: error.message }); + res.status(400).json(response); + return; + } + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; logger.error('[DealerClaimController] Error creating claim request:', error); return ResponseHandler.error(res, 'Failed to create claim request', 500, errorMessage); @@ -541,8 +576,46 @@ export class DealerClaimController { } /** - * Update IO details (Step 3 - Department Lead) + * Validate/Fetch IO details from SAP + * GET /api/v1/dealer-claims/:requestId/io/validate?ioNumber=IO1234 + * This endpoint fetches IO details from SAP and returns them, does not store anything + * Flow: Fetch from SAP -> Return to frontend (no database storage) + */ + async validateIO(req: AuthenticatedRequest, res: Response): Promise { + try { + const { ioNumber } = req.query; + + if (!ioNumber || typeof ioNumber !== 'string') { + return ResponseHandler.error(res, 'IO number is required', 400); + } + + // Fetch IO details from SAP (will return mock data until SAP is integrated) + const ioValidation = await sapIntegrationService.validateIONumber(ioNumber.trim()); + + if (!ioValidation.isValid) { + return ResponseHandler.error(res, ioValidation.error || 'Invalid IO number', 400); + } + + return ResponseHandler.success(res, { + ioNumber: ioValidation.ioNumber, + availableBalance: ioValidation.availableBalance, + blockedAmount: ioValidation.blockedAmount, + remainingBalance: ioValidation.remainingBalance, + currency: ioValidation.currency, + description: ioValidation.description, + isValid: true, + }, 'IO fetched successfully from SAP'); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + logger.error('[DealerClaimController] Error validating IO:', error); + return ResponseHandler.error(res, 'Failed to fetch IO from SAP', 500, errorMessage); + } + } + + /** + * Update IO details and block amount in SAP * PUT /api/v1/dealer-claims/:requestId/io + * Only stores data when blocking amount > 0 * Accepts either UUID or requestNumber */ async updateIODetails(req: AuthenticatedRequest, res: Response): Promise { @@ -568,23 +641,36 @@ export class DealerClaimController { return ResponseHandler.error(res, 'Invalid workflow request', 400); } - if (!ioNumber || availableBalance === undefined || blockedAmount === undefined) { - return ResponseHandler.error(res, 'Missing required IO fields', 400); + if (!ioNumber) { + return ResponseHandler.error(res, 'IO number is required', 400); } - await this.dealerClaimService.updateIODetails( - requestId, - { - ioNumber, - ioRemark: ioRemark || '', - availableBalance: parseFloat(availableBalance), - blockedAmount: parseFloat(blockedAmount), - remainingBalance: remainingBalance ? parseFloat(remainingBalance) : parseFloat(availableBalance) - parseFloat(blockedAmount), - }, - userId - ); + const blockAmount = blockedAmount ? parseFloat(blockedAmount) : 0; + + // Only store in database when blocking amount > 0 + if (blockAmount > 0) { + if (availableBalance === undefined) { + return ResponseHandler.error(res, 'Available balance is required when blocking amount', 400); + } - return ResponseHandler.success(res, { message: 'IO details updated successfully' }, 'IO details updated'); + await this.dealerClaimService.updateIODetails( + requestId, + { + ioNumber, + ioRemark: ioRemark || '', + availableBalance: parseFloat(availableBalance), + blockedAmount: blockAmount, + remainingBalance: remainingBalance ? parseFloat(remainingBalance) : parseFloat(availableBalance) - blockAmount, + }, + userId + ); + + return ResponseHandler.success(res, { message: 'IO blocked successfully in SAP' }, 'IO blocked'); + } else { + // Just validate IO number without storing + // This is for validation only (fetch amount scenario) + return ResponseHandler.success(res, { message: 'IO validated successfully' }, 'IO validated'); + } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; logger.error('[DealerClaimController] Error updating IO details:', error); diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index 51aceba..a807e85 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -36,6 +36,39 @@ export class UserController { } } + /** + * Search users in Okta by displayName + * GET /api/v1/users/search-by-displayname?displayName=John Doe + * Used when creating claim requests to find manager by displayName + */ + async searchByDisplayName(req: Request, res: Response): Promise { + try { + const displayName = String(req.query.displayName || '').trim(); + + if (!displayName) { + ResponseHandler.error(res, 'displayName query parameter is required', 400); + return; + } + + const oktaUsers = await this.userService.searchOktaByDisplayName(displayName); + + const result = oktaUsers.map(u => ({ + userId: u.id, + email: u.profile.email || u.profile.login, + displayName: u.profile.displayName || `${u.profile.firstName || ''} ${u.profile.lastName || ''}`.trim(), + firstName: u.profile.firstName, + lastName: u.profile.lastName, + department: u.profile.department, + status: u.status, + })); + + ResponseHandler.success(res, result, 'Users found by displayName'); + } catch (error: any) { + logger.error('Search by displayName failed', { error }); + ResponseHandler.error(res, error.message || 'Search by displayName failed', 500); + } + } + /** * Ensure user exists in database (create if not exists) * Called when user is selected/tagged in the frontend diff --git a/src/models/WorkflowRequest.ts b/src/models/WorkflowRequest.ts index c2f94e9..bdaffd6 100644 --- a/src/models/WorkflowRequest.ts +++ b/src/models/WorkflowRequest.ts @@ -7,7 +7,7 @@ interface WorkflowRequestAttributes { requestId: string; requestNumber: string; initiatorId: string; - templateType: 'CUSTOM' | 'TEMPLATE'; + templateType: 'CUSTOM' | 'TEMPLATE' | 'DEALER CLAIM'; workflowType?: string; // 'NON_TEMPLATIZED' | 'CLAIM_MANAGEMENT' | etc. templateId?: string; // Reference to workflow_templates if using admin template title: string; @@ -39,7 +39,7 @@ class WorkflowRequest extends Model router.get('/search', authenticateToken, asyncHandler(userController.searchUsers.bind(userController))); +// GET /api/v1/users/search-by-displayname?displayName=John Doe +router.get('/search-by-displayname', authenticateToken, asyncHandler(userController.searchByDisplayName.bind(userController))); + // GET /api/v1/users/configurations - Get public configurations (document policy, workflow sharing, TAT settings) router.get('/configurations', authenticateToken, asyncHandler(getPublicConfigurations)); diff --git a/src/services/dealerClaim.service.ts b/src/services/dealerClaim.service.ts index 3c1f571..be8cecf 100644 --- a/src/services/dealerClaim.service.ts +++ b/src/services/dealerClaim.service.ts @@ -19,6 +19,7 @@ import { sapIntegrationService } from './sapIntegration.service'; import { dmsIntegrationService } from './dmsIntegration.service'; import { notificationService } from './notification.service'; import { activityService } from './activity.service'; +import { UserService } from './user.service'; import logger from '../utils/logger'; /** @@ -28,6 +29,7 @@ import logger from '../utils/logger'; export class DealerClaimService { private workflowService = new WorkflowService(); private approvalService = new ApprovalService(); + private userService = new UserService(); /** * Create a new dealer claim request @@ -48,19 +50,49 @@ export class DealerClaimService { periodStartDate?: Date; periodEndDate?: Date; estimatedBudget?: number; + selectedManagerEmail?: string; // Optional: When multiple managers found, user selects one } ): Promise { try { // Generate request number const requestNumber = await generateRequestNumber(); - // Create workflow request + // First, validate that manager can be resolved BEFORE creating any records + // This ensures no partial records are created if manager is not found + const initiator = await User.findByPk(userId); + if (!initiator) { + throw new Error('Initiator not found'); + } + + // Resolve Department Lead/Manager BEFORE creating workflow + let departmentLead: User | null = null; + + if (claimData.selectedManagerEmail) { + // User selected a manager from multiple options + logger.info(`[DealerClaimService] Using selected manager email: ${claimData.selectedManagerEmail}`); + departmentLead = await this.userService.ensureUserExists({ + email: claimData.selectedManagerEmail, + }); + } else { + // Search Okta using manager displayName from initiator's user record + departmentLead = await this.resolveDepartmentLeadFromManager(initiator); + + // If no manager found, throw error BEFORE creating any records + if (!departmentLead) { + const managerDisplayName = initiator.manager || 'Unknown'; + const error: any = new Error(`No reporting manager found for displayName: "${managerDisplayName}". Please ensure your manager is correctly configured in the system.`); + error.code = 'NO_MANAGER_FOUND'; + throw error; + } + } + + // Now create workflow request (manager is validated) // For claim management, requests are submitted immediately (not drafts) // Step 1 will be active for dealer to submit proposal const workflowRequest = await WorkflowRequest.create({ initiatorId: userId, requestNumber, - templateType: 'CUSTOM', + templateType: 'DEALER CLAIM', // Set template type for dealer claim management workflowType: 'CLAIM_MANAGEMENT', title: `${claimData.activityName} - Claim Request`, description: claimData.requestDescription, @@ -98,7 +130,8 @@ export class DealerClaimService { }); // Create 8 approval levels for claim management workflow - await this.createClaimApprovalLevels(workflowRequest.requestId, userId, claimData.dealerEmail); + // Pass the already-resolved departmentLead to avoid re-searching + await this.createClaimApprovalLevels(workflowRequest.requestId, userId, claimData.dealerEmail, claimData.selectedManagerEmail, departmentLead); // Create participants (initiator, dealer, department lead, finance - exclude system) await this.createClaimParticipants(workflowRequest.requestId, userId, claimData.dealerEmail); @@ -115,35 +148,47 @@ export class DealerClaimService { * Create 8-step approval levels for claim management * Maps approvers based on step requirements: * - Step 1 & 5: Dealer (external user via email) - * - Step 2 & 6: Initiator (requestor) - * - Step 3: Department Lead (resolved from initiator's department/manager) + * - Step 2, 6 & 8: Initiator (requestor) - Step 8 is credit note action taken by initiator + * - Step 3: Department Lead/Manager (resolved from initiator's manager displayName via Okta search) * - Step 4 & 7: System (auto-processed) - * - Step 8: Finance Team (resolved from department/role) */ private async createClaimApprovalLevels( requestId: string, initiatorId: string, - dealerEmail?: string + dealerEmail?: string, + selectedManagerEmail?: string, + departmentLead?: User | null // Pre-resolved department lead (to avoid re-searching) ): Promise { const initiator = await User.findByPk(initiatorId); if (!initiator) { throw new Error('Initiator not found'); } - // Resolve Department Lead for Step 3 - const departmentLead = await this.resolveDepartmentLead(initiator); + // Use pre-resolved department lead if provided, otherwise resolve it + let finalDepartmentLead: User | null = departmentLead || null; - // Resolve Finance Team for Step 8 - const financeApprover = await this.resolveFinanceApprover(); + if (!finalDepartmentLead) { + if (selectedManagerEmail) { + // User selected a manager from multiple options + logger.info(`[DealerClaimService] Using selected manager email: ${selectedManagerEmail}`); + const managerUser = await this.userService.ensureUserExists({ + email: selectedManagerEmail, + }); + finalDepartmentLead = managerUser; + } else { + // Search Okta using manager displayName from initiator's user record + finalDepartmentLead = await this.resolveDepartmentLeadFromManager(initiator); + } + } // Step 1: Dealer Proposal Submission (72 hours) - Dealer submits proposal // Step 2: Requestor Evaluation (48 hours) - Initiator evaluates - // Step 3: Department Lead Approval (72 hours) - Department Lead approves and blocks IO + // Step 3: Department Lead Approval (72 hours) - Department Lead/Manager approves and blocks IO // Step 4: Activity Creation (Auto - 1 hour) - System auto-processes // Step 5: Dealer Completion Documents (120 hours) - Dealer submits completion docs // Step 6: Requestor Claim Approval (48 hours) - Initiator approves completion // Step 7: E-Invoice Generation (Auto - 1 hour) - System generates via DMS - // Step 8: Credit Note Confirmation (48 hours) - Finance confirms credit note (FINAL) + // Step 8: Credit Note Confirmation (48 hours) - Initiator confirms credit note (FINAL) const steps = [ { @@ -214,9 +259,9 @@ export class DealerClaimService { name: 'Credit Note Confirmation', tatHours: 48, isAuto: false, - approverType: 'finance' as const, - approverId: financeApprover?.userId || null, - approverEmail: financeApprover?.email || 'finance@royalenfield.com', + approverType: 'initiator' as const, + approverId: initiatorId, + approverEmail: initiator.email, }, ]; @@ -251,28 +296,16 @@ export class DealerClaimService { approverName = initiator.displayName || initiator.email || 'Requestor'; approverEmail = initiator.email; } else if (step.approverType === 'department_lead') { - if (departmentLead) { - approverId = departmentLead.userId; - approverName = departmentLead.displayName || departmentLead.email || 'Department Lead'; - approverEmail = departmentLead.email; + if (finalDepartmentLead) { + approverId = finalDepartmentLead.userId; + approverName = finalDepartmentLead.displayName || finalDepartmentLead.email || 'Department Lead'; + approverEmail = finalDepartmentLead.email; } else { - // Department lead not found - use initiator as fallback - logger.warn(`[DealerClaimService] Department lead not found for department ${initiator.department}, using initiator as fallback`); - approverId = initiatorId; - approverName = 'Department Lead (Not Found)'; - approverEmail = initiator.email; - } - } else if (step.approverType === 'finance') { - if (financeApprover) { - approverId = financeApprover.userId; - approverName = financeApprover.displayName || financeApprover.email || 'Finance Team'; - approverEmail = financeApprover.email; - } else { - // Finance approver not found - use initiator as fallback - logger.warn(`[DealerClaimService] Finance approver not found, using initiator as fallback`); - approverId = initiatorId; - approverName = 'Finance Team (Not Found)'; - approverEmail = initiator.email; + // This should never happen as we validate manager before creating records + // But keeping as safety check + const error: any = new Error('Department lead not found. This should have been validated before creating the request.'); + error.code = 'DEPARTMENT_LEAD_NOT_FOUND'; + throw error; } } @@ -442,6 +475,96 @@ export class DealerClaimService { * Resolve Department Lead based on initiator's department/manager * If multiple users found with same department, uses the first one */ + /** + * Resolve Department Lead/Manager by searching Okta using manager's displayName + * Flow: + * 1. Get manager displayName from initiator's user record + * 2. Search Okta directory by displayName + * 3. If empty: Return null (no manager found, fallback to old method) + * 4. If single: Use that user, create in DB if doesn't exist, return user + * 5. If multiple: Throw error with list of users (frontend will show confirmation) + * + * @param initiator - The user creating the claim request + * @returns User object for department lead/manager, or null if not found + * @throws Error if multiple managers found (frontend should handle confirmation) + */ + private async resolveDepartmentLeadFromManager(initiator: User): Promise { + try { + // Get manager displayName from initiator's user record + const managerDisplayName = initiator.manager; // This is the displayName of the manager + + if (!managerDisplayName) { + logger.warn(`[DealerClaimService] Initiator ${initiator.email} has no manager displayName set`); + // Return null - caller will handle the error + return null; + } + + logger.info(`[DealerClaimService] Searching Okta for manager with displayName: "${managerDisplayName}"`); + + // Search Okta by displayName + const oktaUsers = await this.userService.searchOktaByDisplayName(managerDisplayName); + + if (oktaUsers.length === 0) { + logger.warn(`[DealerClaimService] No reporting manager found in Okta for displayName: "${managerDisplayName}"`); + // Return null - caller will handle the error + return null; + } + + if (oktaUsers.length === 1) { + // Single match - use this user + const oktaUser = oktaUsers[0]; + const managerEmail = oktaUser.profile.email || oktaUser.profile.login; + + logger.info(`[DealerClaimService] Found single manager match: ${managerEmail} for displayName: "${managerDisplayName}"`); + + // Check if user exists in DB, create if doesn't exist + const managerUser = await this.userService.ensureUserExists({ + userId: oktaUser.id, + email: managerEmail, + displayName: oktaUser.profile.displayName || `${oktaUser.profile.firstName || ''} ${oktaUser.profile.lastName || ''}`.trim(), + firstName: oktaUser.profile.firstName, + lastName: oktaUser.profile.lastName, + department: oktaUser.profile.department, + phone: oktaUser.profile.mobilePhone, + }); + + return managerUser; + } + + // Multiple matches - throw error with list for frontend confirmation + const managerOptions = oktaUsers.map(u => ({ + userId: u.id, + email: u.profile.email || u.profile.login, + displayName: u.profile.displayName || `${u.profile.firstName || ''} ${u.profile.lastName || ''}`.trim(), + firstName: u.profile.firstName, + lastName: u.profile.lastName, + department: u.profile.department, + })); + + logger.warn(`[DealerClaimService] Multiple managers found (${oktaUsers.length}) for displayName: "${managerDisplayName}"`); + + // Create a custom error with the manager options + const error: any = new Error(`Multiple reporting managers found. Please select one.`); + error.code = 'MULTIPLE_MANAGERS_FOUND'; + error.managers = managerOptions; + throw error; + + } catch (error: any) { + // If it's our custom multiple managers error, re-throw it + if (error.code === 'MULTIPLE_MANAGERS_FOUND') { + throw error; + } + + // For other errors, log and fallback to old method + logger.error(`[DealerClaimService] Error resolving manager from Okta:`, error); + return await this.resolveDepartmentLead(initiator); + } + } + + /** + * Legacy method: Resolve Department Lead using old logic + * Kept as fallback when Okta search fails or manager displayName not set + */ private async resolveDepartmentLead(initiator: User): Promise { try { const { Op } = await import('sequelize'); @@ -1007,6 +1130,11 @@ export class DealerClaimService { * Update IO details (Step 3 - Department Lead) * Validates IO number with SAP and blocks budget */ + /** + * Update IO details and block amount in SAP + * Only stores data when blocking amount > 0 + * This method is called when user actually blocks the amount + */ async updateIODetails( requestId: string, ioData: { @@ -1019,6 +1147,13 @@ export class DealerClaimService { organizedByUserId?: string ): Promise { try { + const blockedAmount = ioData.blockedAmount || 0; + + // Only proceed if blocking amount > 0 + if (blockedAmount <= 0) { + throw new Error('Blocked amount must be greater than 0'); + } + // Validate IO number with SAP const ioValidation = await sapIntegrationService.validateIONumber(ioData.ioNumber); @@ -1026,41 +1161,37 @@ export class DealerClaimService { throw new Error(`Invalid IO number: ${ioValidation.error || 'IO number not found in SAP'}`); } - // If amount is provided, block budget in SAP - let blockedAmount = ioData.blockedAmount || 0; - let remainingBalance = ioValidation.remainingBalance; + // Block budget in SAP + const request = await WorkflowRequest.findByPk(requestId); + const requestNumber = request ? ((request as any).requestNumber || (request as any).request_number) : 'UNKNOWN'; + + const blockResult = await sapIntegrationService.blockBudget( + ioData.ioNumber, + blockedAmount, + requestNumber, + `Budget block for claim request ${requestNumber}` + ); - if (ioData.blockedAmount && ioData.blockedAmount > 0) { - const request = await WorkflowRequest.findByPk(requestId); - const requestNumber = request ? ((request as any).requestNumber || (request as any).request_number) : 'UNKNOWN'; - - const blockResult = await sapIntegrationService.blockBudget( - ioData.ioNumber, - ioData.blockedAmount, - requestNumber, - `Budget block for claim request ${requestNumber}` - ); - - if (!blockResult.success) { - throw new Error(`Failed to block budget in SAP: ${blockResult.error}`); - } - - blockedAmount = blockResult.blockedAmount; - remainingBalance = blockResult.remainingBalance; + if (!blockResult.success) { + throw new Error(`Failed to block budget in SAP: ${blockResult.error}`); } - // Get the user who is organizing the IO + const finalBlockedAmount = blockResult.blockedAmount; + const remainingBalance = blockResult.remainingBalance; + const availableBalance = ioData.availableBalance || ioValidation.availableBalance; + + // Get the user who is blocking the IO (current user) const organizedBy = organizedByUserId || null; - // Create or update Internal Order record + // Create or update Internal Order record (only when blocking) const [internalOrder, created] = await InternalOrder.findOrCreate({ where: { requestId }, defaults: { requestId, ioNumber: ioData.ioNumber, ioRemark: ioData.ioRemark || '', - ioAvailableBalance: ioValidation.availableBalance, - ioBlockedAmount: blockedAmount, + ioAvailableBalance: availableBalance, + ioBlockedAmount: finalBlockedAmount, ioRemainingBalance: remainingBalance, organizedBy: organizedBy || undefined, organizedAt: new Date(), @@ -1073,11 +1204,12 @@ export class DealerClaimService { await internalOrder.update({ ioNumber: ioData.ioNumber, ioRemark: ioData.ioRemark || '', - ioAvailableBalance: ioValidation.availableBalance, - ioBlockedAmount: blockedAmount, + ioAvailableBalance: availableBalance, + ioBlockedAmount: finalBlockedAmount, ioRemainingBalance: remainingBalance, + // Update to current user who is blocking organizedBy: organizedBy || internalOrder.organizedBy, - organizedAt: internalOrder.organizedAt || new Date(), + organizedAt: new Date(), status: IOStatus.BLOCKED, }); } @@ -1085,19 +1217,19 @@ export class DealerClaimService { // Update budget tracking with blocked amount await ClaimBudgetTracking.upsert({ requestId, - ioBlockedAmount: blockedAmount, + ioBlockedAmount: finalBlockedAmount, ioBlockedAt: new Date(), budgetStatus: BudgetStatus.BLOCKED, currency: 'INR', }); - logger.info(`[DealerClaimService] IO details updated for request: ${requestId}`, { + logger.info(`[DealerClaimService] IO blocked for request: ${requestId}`, { ioNumber: ioData.ioNumber, - blockedAmount, + blockedAmount: finalBlockedAmount, remainingBalance }); } catch (error) { - logger.error('[DealerClaimService] Error updating IO details:', error); + logger.error('[DealerClaimService] Error blocking IO:', error); throw error; } } diff --git a/src/services/user.service.ts b/src/services/user.service.ts index cac66ec..d7da94d 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -2,6 +2,7 @@ import { User as UserModel } from '../models/User'; import { Op } from 'sequelize'; import { SSOUserData } from '../types/auth.types'; // Use shared type import axios from 'axios'; +import logger from '../utils/logger'; // Using UserModel type directly - interface removed to avoid duplication @@ -320,6 +321,45 @@ export class UserService { } } + /** + * Search users in Okta by displayName + * Uses Okta search API: /api/v1/users?search=profile.displayName eq "displayName" + * @param displayName - Display name to search for + * @returns Array of matching users from Okta + */ + async searchOktaByDisplayName(displayName: string): Promise { + try { + const oktaDomain = process.env.OKTA_DOMAIN; + const oktaApiToken = process.env.OKTA_API_TOKEN; + + if (!oktaDomain || !oktaApiToken) { + logger.warn('[UserService] Okta not configured, returning empty array for displayName search'); + return []; + } + + // Search Okta users by displayName + const response = await axios.get(`${oktaDomain}/api/v1/users`, { + params: { + search: `profile.displayName eq "${displayName}"`, + limit: 50 + }, + headers: { + 'Authorization': `SSWS ${oktaApiToken}`, + 'Accept': 'application/json' + }, + timeout: 5000 + }); + + const oktaUsers: OktaUser[] = response.data || []; + + // Filter only active users + return oktaUsers.filter(u => u.status === 'ACTIVE'); + } catch (error: any) { + logger.error(`[UserService] Error searching Okta by displayName "${displayName}":`, error.message); + return []; + } + } + /** * Fetch user from Okta by email (legacy method, kept for backward compatibility) * @deprecated Use fetchAndExtractOktaUserByEmail instead for full profile extraction