reporting manager is picked using display name while creating claim request

This commit is contained in:
laxmanhalaki 2025-12-12 21:39:52 +05:30
parent d5e195d507
commit ee361a0c4b
7 changed files with 388 additions and 87 deletions

View File

@ -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<void> {
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<void> {
@ -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);

View File

@ -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<void> {
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

View File

@ -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<WorkflowRequestAttributes, WorkflowRequestCr
public requestId!: string;
public requestNumber!: string;
public initiatorId!: string;
public templateType!: 'CUSTOM' | 'TEMPLATE';
public templateType!: 'CUSTOM' | 'TEMPLATE' | 'DEALER CLAIM';
public workflowType?: string;
public templateId?: string;
public title!: string;

View File

@ -58,9 +58,16 @@ router.post('/:requestId/completion', authenticateToken, upload.fields([
{ name: 'attendanceSheet', maxCount: 1 },
]), asyncHandler(dealerClaimController.submitCompletion.bind(dealerClaimController)));
/**
* @route GET /api/v1/dealer-claims/:requestId/io/validate
* @desc Validate/Fetch IO details from SAP (returns dummy data for now)
* @access Private
*/
router.get('/:requestId/io/validate', authenticateToken, asyncHandler(dealerClaimController.validateIO.bind(dealerClaimController)));
/**
* @route PUT /api/v1/dealer-claims/:requestId/io
* @desc Update IO details (Step 3)
* @desc Block IO amount in SAP and store in database
* @access Private
*/
router.put('/:requestId/io', authenticateToken, asyncHandler(dealerClaimController.updateIODetails.bind(dealerClaimController)));

View File

@ -10,6 +10,9 @@ const userController = new UserController();
// GET /api/v1/users/search?q=<email or name>
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));

View File

@ -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<WorkflowRequest> {
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<void> {
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<User | null> {
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<User | null> {
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<void> {
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;
}
}

View File

@ -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<OktaUser[]> {
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