dealer claim multi iteration implementaion started
This commit is contained in:
parent
f89514eb2b
commit
e3bda6df15
@ -250,6 +250,7 @@ export class DealerClaimController {
|
||||
numberOfParticipants,
|
||||
closedExpenses,
|
||||
totalClosedExpenses,
|
||||
completionDescription,
|
||||
} = req.body;
|
||||
|
||||
// Parse closedExpenses if it's a JSON string
|
||||
@ -540,6 +541,7 @@ export class DealerClaimController {
|
||||
totalClosedExpenses: totalClosedExpenses ? parseFloat(totalClosedExpenses) : 0,
|
||||
invoicesReceipts: invoicesReceipts.length > 0 ? invoicesReceipts : undefined,
|
||||
attendanceSheet: attendanceSheet || undefined,
|
||||
completionDescription: completionDescription || undefined,
|
||||
});
|
||||
|
||||
return ResponseHandler.success(res, { message: 'Completion documents submitted successfully' }, 'Completion submitted');
|
||||
|
||||
@ -13,9 +13,11 @@ import path from 'path';
|
||||
import crypto from 'crypto';
|
||||
import { getRequestMetadata } from '@utils/requestUtils';
|
||||
import { enrichApprovalLevels, enrichSpectators, validateInitiator } from '@services/userEnrichment.service';
|
||||
import { DealerClaimService } from '@services/dealerClaim.service';
|
||||
import logger from '@utils/logger';
|
||||
|
||||
const workflowService = new WorkflowService();
|
||||
const dealerClaimService = new DealerClaimService();
|
||||
|
||||
export class WorkflowController {
|
||||
async createWorkflow(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
@ -886,4 +888,54 @@ export class WorkflowController {
|
||||
ResponseHandler.error(res, 'Failed to submit workflow', 400, errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
async handleInitiatorAction(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { action, ...data } = req.body;
|
||||
const userId = req.user?.userId;
|
||||
|
||||
if (!userId) {
|
||||
ResponseHandler.unauthorized(res, 'User ID missing from request');
|
||||
return;
|
||||
}
|
||||
|
||||
await dealerClaimService.handleInitiatorAction(id, userId, action as any, data);
|
||||
ResponseHandler.success(res, null, `Action ${action} performed successfully`);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
logger.error('[WorkflowController] handleInitiatorAction failed', {
|
||||
error: errorMessage,
|
||||
requestId: req.params.id,
|
||||
userId: req.user?.userId,
|
||||
action: req.body.action
|
||||
});
|
||||
ResponseHandler.error(res, 'Failed to perform initiator action', 400, errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
async getHistory(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// Resolve requestId UUID from identifier (could be requestNumber or UUID)
|
||||
const workflowService = new WorkflowService();
|
||||
const wf = await (workflowService as any).findWorkflowByIdentifier(id);
|
||||
if (!wf) {
|
||||
ResponseHandler.notFound(res, 'Workflow not found');
|
||||
return;
|
||||
}
|
||||
const requestId = wf.getDataValue('requestId');
|
||||
|
||||
const history = await dealerClaimService.getHistory(requestId);
|
||||
ResponseHandler.success(res, history, 'Revision history fetched successfully');
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
logger.error('[WorkflowController] getHistory failed', {
|
||||
error: errorMessage,
|
||||
requestId: req.params.id
|
||||
});
|
||||
ResponseHandler.error(res, 'Failed to fetch revision history', 400, errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
136
src/migrations/20260113-redesign-dealer-claim-history.ts
Normal file
136
src/migrations/20260113-redesign-dealer-claim-history.ts
Normal file
@ -0,0 +1,136 @@
|
||||
import { QueryInterface, DataTypes } from 'sequelize';
|
||||
|
||||
export const up = async (queryInterface: QueryInterface) => {
|
||||
// 1. Drop the old dealer_claim_history table if it exists
|
||||
const tables = await queryInterface.showAllTables();
|
||||
if (tables.includes('dealer_claim_history')) {
|
||||
await queryInterface.dropTable('dealer_claim_history');
|
||||
}
|
||||
|
||||
// 2. Create or update the enum type for snapshot_type
|
||||
// Check if enum exists, if not create it, if yes update it
|
||||
try {
|
||||
await queryInterface.sequelize.query(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'enum_dealer_claim_history_snapshot_type') THEN
|
||||
CREATE TYPE enum_dealer_claim_history_snapshot_type AS ENUM ('PROPOSAL', 'COMPLETION', 'INTERNAL_ORDER', 'WORKFLOW', 'APPROVE');
|
||||
ELSE
|
||||
-- Check if APPROVE exists in the enum
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_enum
|
||||
WHERE enumlabel = 'APPROVE'
|
||||
AND enumtypid = (SELECT oid FROM pg_type WHERE typname = 'enum_dealer_claim_history_snapshot_type')
|
||||
) THEN
|
||||
ALTER TYPE enum_dealer_claim_history_snapshot_type ADD VALUE 'APPROVE';
|
||||
END IF;
|
||||
END IF;
|
||||
END $$;
|
||||
`);
|
||||
} catch (error) {
|
||||
// If enum creation fails, try to continue (might already exist)
|
||||
console.warn('Enum creation/update warning:', error);
|
||||
}
|
||||
|
||||
// 3. Create new simplified level-based dealer_claim_history table
|
||||
await queryInterface.createTable('dealer_claim_history', {
|
||||
history_id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
request_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'workflow_requests',
|
||||
key: 'request_id'
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'CASCADE'
|
||||
},
|
||||
approval_level_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: true, // Nullable for workflow-level snapshots
|
||||
references: {
|
||||
model: 'approval_levels',
|
||||
key: 'level_id'
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'SET NULL'
|
||||
},
|
||||
level_number: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true, // Nullable for workflow-level snapshots
|
||||
comment: 'Level number for easier querying (e.g., 1=Dealer, 3=Dept Lead, 4/5=Completion)'
|
||||
},
|
||||
level_name: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true, // Nullable for workflow-level snapshots
|
||||
comment: 'Level name for consistent matching (e.g., "Dealer Proposal Submission", "Department Lead Approval")'
|
||||
},
|
||||
version: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: 'Version number for this specific level (starts at 1 per level)'
|
||||
},
|
||||
snapshot_type: {
|
||||
type: DataTypes.ENUM('PROPOSAL', 'COMPLETION', 'INTERNAL_ORDER', 'WORKFLOW', 'APPROVE'),
|
||||
allowNull: false,
|
||||
comment: 'Type of snapshot: PROPOSAL (Step 1), COMPLETION (Step 4/5), INTERNAL_ORDER (Step 3), WORKFLOW (general), APPROVE (approver actions with comments)'
|
||||
},
|
||||
snapshot_data: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: false,
|
||||
comment: 'JSON object containing all snapshot data specific to this level and type. Structure varies by snapshot_type.'
|
||||
},
|
||||
change_reason: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: 'Reason for this version change (e.g., "Revision Requested: ...")'
|
||||
},
|
||||
changed_by: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'user_id'
|
||||
}
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
}
|
||||
});
|
||||
|
||||
// Add indexes for efficient querying
|
||||
await queryInterface.addIndex('dealer_claim_history', ['request_id', 'level_number', 'version'], {
|
||||
name: 'idx_history_request_level_version'
|
||||
});
|
||||
await queryInterface.addIndex('dealer_claim_history', ['approval_level_id', 'version'], {
|
||||
name: 'idx_history_level_version'
|
||||
});
|
||||
await queryInterface.addIndex('dealer_claim_history', ['request_id', 'snapshot_type'], {
|
||||
name: 'idx_history_request_type'
|
||||
});
|
||||
await queryInterface.addIndex('dealer_claim_history', ['snapshot_type', 'level_number'], {
|
||||
name: 'idx_history_type_level'
|
||||
});
|
||||
await queryInterface.addIndex('dealer_claim_history', ['request_id', 'level_name'], {
|
||||
name: 'idx_history_request_level_name'
|
||||
});
|
||||
await queryInterface.addIndex('dealer_claim_history', ['level_name', 'snapshot_type'], {
|
||||
name: 'idx_history_level_name_type'
|
||||
});
|
||||
// Index for JSONB queries on snapshot_data
|
||||
await queryInterface.addIndex('dealer_claim_history', ['snapshot_type'], {
|
||||
name: 'idx_history_snapshot_type',
|
||||
using: 'BTREE'
|
||||
});
|
||||
};
|
||||
|
||||
export const down = async (queryInterface: QueryInterface) => {
|
||||
// Drop the new table
|
||||
await queryInterface.dropTable('dealer_claim_history');
|
||||
};
|
||||
190
src/models/DealerClaimHistory.ts
Normal file
190
src/models/DealerClaimHistory.ts
Normal file
@ -0,0 +1,190 @@
|
||||
import { DataTypes, Model, Optional } from 'sequelize';
|
||||
import { sequelize } from '@config/database';
|
||||
import { WorkflowRequest } from './WorkflowRequest';
|
||||
import { ApprovalLevel } from './ApprovalLevel';
|
||||
import { User } from './User';
|
||||
|
||||
export enum SnapshotType {
|
||||
PROPOSAL = 'PROPOSAL',
|
||||
COMPLETION = 'COMPLETION',
|
||||
INTERNAL_ORDER = 'INTERNAL_ORDER',
|
||||
WORKFLOW = 'WORKFLOW',
|
||||
APPROVE = 'APPROVE'
|
||||
}
|
||||
|
||||
// Type definitions for snapshot data structures
|
||||
export interface ProposalSnapshotData {
|
||||
documentUrl?: string;
|
||||
totalBudget?: number;
|
||||
comments?: string;
|
||||
expectedCompletionDate?: string;
|
||||
costItems?: Array<{
|
||||
description: string;
|
||||
amount: number;
|
||||
order: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface CompletionSnapshotData {
|
||||
documentUrl?: string;
|
||||
totalExpenses?: number;
|
||||
comments?: string;
|
||||
expenses?: Array<{
|
||||
description: string;
|
||||
amount: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface IOSnapshotData {
|
||||
ioNumber?: string;
|
||||
blockedAmount?: number;
|
||||
availableBalance?: number;
|
||||
remainingBalance?: number;
|
||||
sapDocumentNumber?: string;
|
||||
}
|
||||
|
||||
export interface WorkflowSnapshotData {
|
||||
status?: string;
|
||||
currentLevel?: number;
|
||||
}
|
||||
|
||||
export interface ApprovalSnapshotData {
|
||||
action: 'APPROVE' | 'REJECT';
|
||||
comments?: string;
|
||||
rejectionReason?: string;
|
||||
approverName?: string;
|
||||
approverEmail?: string;
|
||||
levelName?: string;
|
||||
}
|
||||
|
||||
interface DealerClaimHistoryAttributes {
|
||||
historyId: string;
|
||||
requestId: string;
|
||||
approvalLevelId?: string;
|
||||
levelNumber?: number;
|
||||
levelName?: string;
|
||||
version: number;
|
||||
snapshotType: SnapshotType;
|
||||
snapshotData: ProposalSnapshotData | CompletionSnapshotData | IOSnapshotData | WorkflowSnapshotData | ApprovalSnapshotData | any;
|
||||
changeReason?: string;
|
||||
changedBy: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
interface DealerClaimHistoryCreationAttributes extends Optional<DealerClaimHistoryAttributes, 'historyId' | 'approvalLevelId' | 'levelNumber' | 'levelName' | 'changeReason' | 'createdAt'> { }
|
||||
|
||||
class DealerClaimHistory extends Model<DealerClaimHistoryAttributes, DealerClaimHistoryCreationAttributes> implements DealerClaimHistoryAttributes {
|
||||
public historyId!: string;
|
||||
public requestId!: string;
|
||||
public approvalLevelId?: string;
|
||||
public levelNumber?: number;
|
||||
public version!: number;
|
||||
public snapshotType!: SnapshotType;
|
||||
public snapshotData!: ProposalSnapshotData | CompletionSnapshotData | IOSnapshotData | WorkflowSnapshotData | any;
|
||||
public changeReason?: string;
|
||||
public changedBy!: string;
|
||||
public createdAt!: Date;
|
||||
}
|
||||
|
||||
DealerClaimHistory.init(
|
||||
{
|
||||
historyId: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
field: 'history_id'
|
||||
},
|
||||
requestId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
field: 'request_id',
|
||||
references: {
|
||||
model: 'workflow_requests',
|
||||
key: 'request_id'
|
||||
}
|
||||
},
|
||||
approvalLevelId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: true,
|
||||
field: 'approval_level_id',
|
||||
references: {
|
||||
model: 'approval_levels',
|
||||
key: 'level_id'
|
||||
}
|
||||
},
|
||||
levelNumber: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
field: 'level_number'
|
||||
},
|
||||
levelName: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
field: 'level_name'
|
||||
},
|
||||
version: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
snapshotType: {
|
||||
type: DataTypes.ENUM('PROPOSAL', 'COMPLETION', 'INTERNAL_ORDER', 'WORKFLOW', 'APPROVE'),
|
||||
allowNull: false,
|
||||
field: 'snapshot_type'
|
||||
},
|
||||
snapshotData: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: false,
|
||||
field: 'snapshot_data'
|
||||
},
|
||||
changeReason: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
field: 'change_reason'
|
||||
},
|
||||
changedBy: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
field: 'changed_by',
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'user_id'
|
||||
}
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
field: 'created_at'
|
||||
}
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'DealerClaimHistory',
|
||||
tableName: 'dealer_claim_history',
|
||||
timestamps: false,
|
||||
indexes: [
|
||||
{
|
||||
fields: ['request_id', 'level_number', 'version'],
|
||||
name: 'idx_history_request_level_version'
|
||||
},
|
||||
{
|
||||
fields: ['approval_level_id', 'version'],
|
||||
name: 'idx_history_level_version'
|
||||
},
|
||||
{
|
||||
fields: ['request_id', 'snapshot_type'],
|
||||
name: 'idx_history_request_type'
|
||||
},
|
||||
{
|
||||
fields: ['snapshot_type', 'level_number'],
|
||||
name: 'idx_history_type_level'
|
||||
}
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
DealerClaimHistory.belongsTo(WorkflowRequest, { foreignKey: 'requestId' });
|
||||
DealerClaimHistory.belongsTo(ApprovalLevel, { foreignKey: 'approvalLevelId' });
|
||||
DealerClaimHistory.belongsTo(User, { as: 'changer', foreignKey: 'changedBy' });
|
||||
|
||||
export { DealerClaimHistory };
|
||||
@ -29,11 +29,12 @@ interface WorkflowRequestAttributes {
|
||||
pauseReason?: string;
|
||||
pauseResumeDate?: Date;
|
||||
pauseTatSnapshot?: any;
|
||||
version: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
interface WorkflowRequestCreationAttributes extends Optional<WorkflowRequestAttributes, 'requestId' | 'submissionDate' | 'closureDate' | 'conclusionRemark' | 'aiGeneratedConclusion' | 'isPaused' | 'pausedAt' | 'pausedBy' | 'pauseReason' | 'pauseResumeDate' | 'pauseTatSnapshot' | 'createdAt' | 'updatedAt'> {}
|
||||
interface WorkflowRequestCreationAttributes extends Optional<WorkflowRequestAttributes, 'requestId' | 'submissionDate' | 'closureDate' | 'conclusionRemark' | 'aiGeneratedConclusion' | 'isPaused' | 'pausedAt' | 'pausedBy' | 'pauseReason' | 'pauseResumeDate' | 'pauseTatSnapshot' | 'version' | 'createdAt' | 'updatedAt'> { }
|
||||
|
||||
class WorkflowRequest extends Model<WorkflowRequestAttributes, WorkflowRequestCreationAttributes> implements WorkflowRequestAttributes {
|
||||
public requestId!: string;
|
||||
@ -61,6 +62,7 @@ class WorkflowRequest extends Model<WorkflowRequestAttributes, WorkflowRequestCr
|
||||
public pauseReason?: string;
|
||||
public pauseResumeDate?: Date;
|
||||
public pauseTatSnapshot?: any;
|
||||
public version!: number;
|
||||
public createdAt!: Date;
|
||||
public updatedAt!: Date;
|
||||
|
||||
@ -211,6 +213,11 @@ WorkflowRequest.init(
|
||||
allowNull: true,
|
||||
field: 'pause_tat_snapshot'
|
||||
},
|
||||
version: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 1,
|
||||
allowNull: false
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
|
||||
@ -25,6 +25,7 @@ import { InternalOrder } from './InternalOrder';
|
||||
import { ClaimBudgetTracking } from './ClaimBudgetTracking';
|
||||
import { Dealer } from './Dealer';
|
||||
import { ActivityType } from './ActivityType';
|
||||
import { DealerClaimHistory } from './DealerClaimHistory';
|
||||
|
||||
// Define associations
|
||||
const defineAssociations = () => {
|
||||
@ -137,6 +138,13 @@ const defineAssociations = () => {
|
||||
sourceKey: 'requestId'
|
||||
});
|
||||
|
||||
// DealerClaimHistory associations
|
||||
WorkflowRequest.hasMany(DealerClaimHistory, {
|
||||
as: 'history',
|
||||
foreignKey: 'requestId',
|
||||
sourceKey: 'requestId'
|
||||
});
|
||||
|
||||
// Note: belongsTo associations are defined in individual model files to avoid duplicate alias conflicts
|
||||
// Only hasMany associations from WorkflowRequest are defined here since they're one-way
|
||||
};
|
||||
@ -170,7 +178,8 @@ export {
|
||||
InternalOrder,
|
||||
ClaimBudgetTracking,
|
||||
Dealer,
|
||||
ActivityType
|
||||
ActivityType,
|
||||
DealerClaimHistory
|
||||
};
|
||||
|
||||
// Export default sequelize instance
|
||||
|
||||
@ -874,4 +874,19 @@ router.get('/:id/pause',
|
||||
asyncHandler(pauseController.getPauseDetails.bind(pauseController))
|
||||
);
|
||||
|
||||
// Initiator actions for rejected/returned requests
|
||||
router.post('/:id/initiator-action',
|
||||
authenticateToken,
|
||||
requireParticipantTypes(['INITIATOR']),
|
||||
validateParams(workflowParamsSchema),
|
||||
asyncHandler(workflowController.handleInitiatorAction.bind(workflowController))
|
||||
);
|
||||
|
||||
// Get revision history
|
||||
router.get('/:id/history',
|
||||
authenticateToken,
|
||||
validateParams(workflowParamsSchema),
|
||||
asyncHandler(workflowController.getHistory.bind(workflowController))
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@ -135,6 +135,7 @@ async function runMigrations(): Promise<void> {
|
||||
const m40 = require('../migrations/20251218-fix-claim-invoice-credit-note-columns');
|
||||
const m41 = require('../migrations/20250120-create-dealers-table');
|
||||
const m42 = require('../migrations/20250125-create-activity-types');
|
||||
const m43 = require('../migrations/20260113-redesign-dealer-claim-history');
|
||||
|
||||
const migrations = [
|
||||
{ name: '2025103000-create-users', module: m0 },
|
||||
@ -182,6 +183,7 @@ async function runMigrations(): Promise<void> {
|
||||
{ name: '20251218-fix-claim-invoice-credit-note-columns', module: m40 },
|
||||
{ name: '20250120-create-dealers-table', module: m41 },
|
||||
{ name: '20250125-create-activity-types', module: m42 },
|
||||
{ name: '20260113-redesign-dealer-claim-history', module: m43 },
|
||||
];
|
||||
|
||||
const queryInterface = sequelize.getQueryInterface();
|
||||
|
||||
@ -45,6 +45,7 @@ import * as m39 from '../migrations/20251214-create-dealer-completion-expenses';
|
||||
import * as m40 from '../migrations/20251218-fix-claim-invoice-credit-note-columns';
|
||||
import * as m41 from '../migrations/20250120-create-dealers-table';
|
||||
import * as m42 from '../migrations/20250125-create-activity-types';
|
||||
import * as m43 from '../migrations/20260113-redesign-dealer-claim-history';
|
||||
|
||||
interface Migration {
|
||||
name: string;
|
||||
@ -104,6 +105,7 @@ const migrations: Migration[] = [
|
||||
{ name: '20251218-fix-claim-invoice-credit-note-columns', module: m40 },
|
||||
{ name: '20250120-create-dealers-table', module: m41 },
|
||||
{ name: '20250125-create-activity-types', module: m42 },
|
||||
{ name: '20260113-redesign-dealer-claim-history', module: m43 },
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -26,6 +26,10 @@ import { DealerClaimService } from './dealerClaim.service';
|
||||
import { emitToRequestRoom } from '../realtime/socket';
|
||||
|
||||
export class DealerClaimApprovalService {
|
||||
// Use lazy initialization to avoid circular dependency
|
||||
private getDealerClaimService(): DealerClaimService {
|
||||
return new DealerClaimService();
|
||||
}
|
||||
/**
|
||||
* Approve a level in a dealer claim workflow
|
||||
* Handles dealer claim-specific logic including dynamic approvers and activity creation
|
||||
@ -102,6 +106,34 @@ export class DealerClaimApprovalService {
|
||||
return await this.handleRejection(level, action, userId, requestMetadata, elapsedHours, tatPercentage, now);
|
||||
}
|
||||
|
||||
// Save approval history BEFORE updating level
|
||||
await this.getDealerClaimService().saveApprovalHistory(
|
||||
level.requestId,
|
||||
level.levelId,
|
||||
level.levelNumber,
|
||||
'APPROVE',
|
||||
action.comments || '',
|
||||
undefined,
|
||||
userId
|
||||
);
|
||||
|
||||
// Capture workflow snapshot for approval action (before moving to next level)
|
||||
// This captures the approval action itself, including initiator evaluation
|
||||
const levelName = (level.levelName || '').toLowerCase();
|
||||
const isInitiatorEvaluation = levelName.includes('requestor') || levelName.includes('evaluation');
|
||||
const approvalMessage = isInitiatorEvaluation
|
||||
? `Initiator evaluated and approved (level ${level.levelNumber})`
|
||||
: `Approved level ${level.levelNumber}`;
|
||||
|
||||
await this.getDealerClaimService().saveWorkflowHistory(
|
||||
level.requestId,
|
||||
approvalMessage,
|
||||
userId,
|
||||
level.levelId,
|
||||
level.levelNumber,
|
||||
level.levelName || undefined
|
||||
);
|
||||
|
||||
// Update level status and elapsed time for approval
|
||||
await level.update({
|
||||
status: ApprovalStatus.APPROVED,
|
||||
@ -242,6 +274,17 @@ export class DealerClaimApprovalService {
|
||||
{ currentLevel: nextLevelNumber },
|
||||
{ where: { requestId: level.requestId } }
|
||||
);
|
||||
|
||||
// Capture workflow snapshot when moving to next level
|
||||
// Include both the approved level and the next level in the message
|
||||
await this.getDealerClaimService().saveWorkflowHistory(
|
||||
level.requestId,
|
||||
`Level ${level.levelNumber} approved, moved to next level (${nextLevelNumber})`,
|
||||
userId,
|
||||
nextLevel?.levelId || undefined, // Store next level's ID since we're moving to it
|
||||
nextLevelNumber || undefined,
|
||||
(nextLevel as any)?.levelName || undefined
|
||||
);
|
||||
logger.info(`[DealerClaimApproval] Approved level ${level.levelNumber}. Activated next level ${nextLevelNumber} for workflow ${level.requestId}`);
|
||||
}
|
||||
|
||||
@ -596,6 +639,125 @@ export class DealerClaimApprovalService {
|
||||
const wf = await WorkflowRequest.findByPk(level.requestId);
|
||||
if (!wf) return null;
|
||||
|
||||
// Check if this is the Department Lead approval step (Step 3)
|
||||
// Robust check: check level name for variations and level number as fallback
|
||||
const levelName = (level.levelName || '').toLowerCase();
|
||||
const isDeptLeadResult =
|
||||
levelName.includes('department lead') ||
|
||||
levelName.includes('dept lead');
|
||||
|
||||
if (isDeptLeadResult) {
|
||||
logger.info(`[DealerClaimApproval] Department Lead rejected request ${level.requestId}. Circling back to initiator.`);
|
||||
|
||||
// Save approval history (rejection) BEFORE updating level
|
||||
await this.getDealerClaimService().saveApprovalHistory(
|
||||
level.requestId,
|
||||
level.levelId,
|
||||
level.levelNumber,
|
||||
'REJECT',
|
||||
action.comments || '',
|
||||
action.rejectionReason || undefined,
|
||||
userId
|
||||
);
|
||||
|
||||
// Update level status to REJECTED (but signifies a return at this level)
|
||||
await level.update({
|
||||
status: ApprovalStatus.REJECTED,
|
||||
actionDate: rejectionNow,
|
||||
levelEndTime: rejectionNow,
|
||||
elapsedHours: elapsedHours || 0,
|
||||
tatPercentageUsed: tatPercentage || 0,
|
||||
comments: action.comments || action.rejectionReason || undefined
|
||||
});
|
||||
|
||||
// Create or activate initiator action level
|
||||
const initiatorLevel = await this.getDealerClaimService().createOrActivateInitiatorLevel(
|
||||
level.requestId,
|
||||
(wf as any).initiatorId
|
||||
);
|
||||
|
||||
// Update workflow status to REJECTED but DO NOT set closureDate
|
||||
// Set currentLevel to initiator level if created
|
||||
const newCurrentLevel = initiatorLevel ? initiatorLevel.levelNumber : wf.currentLevel;
|
||||
await WorkflowRequest.update(
|
||||
{
|
||||
status: WorkflowStatus.REJECTED,
|
||||
currentLevel: newCurrentLevel
|
||||
},
|
||||
{ where: { requestId: level.requestId } }
|
||||
);
|
||||
|
||||
// Capture workflow snapshot when moving back to initiator
|
||||
// Include the rejected level information in the message
|
||||
await this.getDealerClaimService().saveWorkflowHistory(
|
||||
level.requestId,
|
||||
`Department Lead rejected (level ${level.levelNumber}) and moved back to initiator (level ${newCurrentLevel})`,
|
||||
userId,
|
||||
level.levelId, // Store the rejected level's ID
|
||||
level.levelNumber, // Store the rejected level's number
|
||||
level.levelName || undefined // Store the rejected level's name
|
||||
);
|
||||
|
||||
// Log activity
|
||||
activityService.log({
|
||||
requestId: level.requestId,
|
||||
type: 'rejection',
|
||||
user: { userId: level.approverId, name: level.approverName },
|
||||
timestamp: rejectionNow.toISOString(),
|
||||
action: 'Returned to Initiator',
|
||||
details: `Request returned to initiator by ${level.approverName || level.approverEmail}. Reason: ${action.rejectionReason || action.comments || 'No reason provided'}`,
|
||||
ipAddress: requestMetadata?.ipAddress || undefined,
|
||||
userAgent: requestMetadata?.userAgent || undefined
|
||||
});
|
||||
|
||||
// Notify ONLY the initiator
|
||||
await notificationService.sendToUsers([(wf as any).initiatorId], {
|
||||
title: `Action Required: Request Returned - ${(wf as any).requestNumber}`,
|
||||
body: `Your request "${(wf as any).title}" has been returned to you by the Department Lead for revision/discussion. Reason: ${action.rejectionReason || action.comments || 'No reason provided'}`,
|
||||
requestNumber: (wf as any).requestNumber,
|
||||
requestId: level.requestId,
|
||||
url: `/request/${(wf as any).requestNumber}`,
|
||||
type: 'rejection',
|
||||
priority: 'HIGH',
|
||||
actionRequired: true
|
||||
});
|
||||
|
||||
// Emit real-time update
|
||||
emitToRequestRoom(level.requestId, 'request:updated', {
|
||||
requestId: level.requestId,
|
||||
requestNumber: (wf as any)?.requestNumber,
|
||||
action: 'RETURN',
|
||||
levelNumber: level.levelNumber,
|
||||
timestamp: rejectionNow.toISOString()
|
||||
});
|
||||
|
||||
return level;
|
||||
}
|
||||
|
||||
// Default terminal rejection logic for other steps
|
||||
logger.info(`[DealerClaimApproval] Standard rejection for request ${level.requestId} by level ${level.levelNumber}`);
|
||||
|
||||
// Save approval history (rejection) BEFORE updating level
|
||||
await this.getDealerClaimService().saveApprovalHistory(
|
||||
level.requestId,
|
||||
level.levelId,
|
||||
level.levelNumber,
|
||||
'REJECT',
|
||||
action.comments || '',
|
||||
action.rejectionReason || undefined,
|
||||
userId
|
||||
);
|
||||
|
||||
// Capture workflow snapshot for terminal rejection action
|
||||
await this.getDealerClaimService().saveWorkflowHistory(
|
||||
level.requestId,
|
||||
`Level ${level.levelNumber} rejected (terminal rejection)`,
|
||||
userId,
|
||||
level.levelId,
|
||||
level.levelNumber,
|
||||
level.levelName || undefined
|
||||
);
|
||||
|
||||
// Update level status
|
||||
await level.update({
|
||||
status: ApprovalStatus.REJECTED,
|
||||
|
||||
@ -21,16 +21,24 @@ interface UploadResult {
|
||||
|
||||
class GCSStorageService {
|
||||
private storage: Storage | null = null;
|
||||
private bucketName: string;
|
||||
private projectId: string;
|
||||
private bucketName: string = '';
|
||||
private projectId: string = '';
|
||||
|
||||
constructor() {
|
||||
// Check if Google Secret Manager should be used
|
||||
const useGoogleSecretManager = process.env.USE_GOOGLE_SECRET_MANAGER === 'true';
|
||||
|
||||
if (!useGoogleSecretManager) {
|
||||
logger.info('[GCS] USE_GOOGLE_SECRET_MANAGER is not enabled. Will use local storage fallback.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.projectId = process.env.GCP_PROJECT_ID || '';
|
||||
this.bucketName = process.env.GCP_BUCKET_NAME || '';
|
||||
const keyFilePath = process.env.GCP_KEY_FILE || '';
|
||||
|
||||
if (!this.projectId || !this.bucketName || !keyFilePath) {
|
||||
logger.warn('[GCS] GCP configuration missing. File uploads will fail.');
|
||||
logger.warn('[GCS] GCP configuration missing. File uploads will use local storage fallback.');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -41,7 +49,7 @@ class GCSStorageService {
|
||||
: path.resolve(process.cwd(), keyFilePath);
|
||||
|
||||
if (!fs.existsSync(resolvedKeyPath)) {
|
||||
logger.error(`[GCS] Key file not found at: ${resolvedKeyPath}`);
|
||||
logger.error(`[GCS] Key file not found at: ${resolvedKeyPath}. Will use local storage fallback.`);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -55,7 +63,7 @@ class GCSStorageService {
|
||||
bucketName: this.bucketName,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[GCS] Failed to initialize:', error);
|
||||
logger.error('[GCS] Failed to initialize. Will use local storage fallback:', error);
|
||||
}
|
||||
}
|
||||
|
||||
@ -341,6 +349,12 @@ class GCSStorageService {
|
||||
* Check if GCS is properly configured
|
||||
*/
|
||||
isConfigured(): boolean {
|
||||
// Check if Google Secret Manager is enabled
|
||||
const useGoogleSecretManager = process.env.USE_GOOGLE_SECRET_MANAGER === 'true';
|
||||
if (!useGoogleSecretManager) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.storage !== null && this.bucketName !== '' && this.projectId !== '';
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user