17 KiB
17 KiB
Extensible Workflow Architecture Plan
Supporting Multiple Template Types (Claim Management, Non-Templatized, Future Templates)
Overview
This document outlines how to design the backend architecture to support:
- Unified Request System: All requests (templatized, non-templatized, claim management) use the same
workflow_requeststable - Template Identification: Distinguish between different workflow types
- Extensibility: Easy addition of new templates by admins without code changes
- Unified Views: All requests appear in "My Requests", "Open Requests", etc. automatically
Architecture Principles
1. Single Source of Truth: workflow_requests Table
All requests, regardless of type, are stored in the same table:
workflow_requests {
request_id UUID PK
request_number VARCHAR(20) UK
initiator_id UUID FK
template_type VARCHAR(20) -- 'CUSTOM' | 'TEMPLATE' (high-level)
workflow_type VARCHAR(50) -- 'NON_TEMPLATIZED' | 'CLAIM_MANAGEMENT' | 'DEALER_ONBOARDING' | etc.
template_id UUID FK (nullable) -- Reference to workflow_templates if using admin template
title VARCHAR(500)
description TEXT
status workflow_status
current_level INTEGER
total_levels INTEGER
-- ... common fields
}
Key Fields:
template_type: High-level classification ('CUSTOM' for user-created, 'TEMPLATE' for admin templates)workflow_type: Specific workflow identifier (e.g., 'CLAIM_MANAGEMENT', 'NON_TEMPLATIZED')template_id: Optional reference toworkflow_templatestable if using an admin-created template
2. Template-Specific Data Storage
Each workflow type can have its own extension table for type-specific data:
-- For Claim Management
dealer_claim_details {
claim_id UUID PK
request_id UUID FK -> workflow_requests(request_id)
activity_name VARCHAR(500)
activity_type VARCHAR(100)
dealer_code VARCHAR(50)
dealer_name VARCHAR(200)
dealer_email VARCHAR(255)
dealer_phone VARCHAR(20)
dealer_address TEXT
activity_date DATE
location VARCHAR(255)
period_start_date DATE
period_end_date DATE
estimated_budget DECIMAL(15,2)
closed_expenses DECIMAL(15,2)
io_number VARCHAR(50)
io_blocked_amount DECIMAL(15,2)
sap_document_number VARCHAR(100)
dms_number VARCHAR(100)
e_invoice_number VARCHAR(100)
credit_note_number VARCHAR(100)
-- ... claim-specific fields
}
-- For Non-Templatized (if needed)
non_templatized_details {
detail_id UUID PK
request_id UUID FK -> workflow_requests(request_id)
custom_fields JSONB -- Flexible storage for any custom data
-- ... any specific fields
}
-- For Future Templates
-- Each new template can have its own extension table
3. Workflow Templates Table (Admin-Created Templates)
workflow_templates {
template_id UUID PK
template_name VARCHAR(200) -- Display name: "Claim Management", "Dealer Onboarding"
template_code VARCHAR(50) UK -- Unique identifier: "CLAIM_MANAGEMENT", "DEALER_ONBOARDING"
template_description TEXT
template_category VARCHAR(100) -- "Dealer Operations", "HR", "Finance", etc.
workflow_type VARCHAR(50) -- Maps to workflow_requests.workflow_type
approval_levels_config JSONB -- Step definitions, TAT, roles, etc.
default_tat_hours DECIMAL(10,2)
form_fields_config JSONB -- Form field definitions for wizard
is_active BOOLEAN
is_system_template BOOLEAN -- True for built-in (Claim Management), False for admin-created
created_by UUID FK
created_at TIMESTAMP
updated_at TIMESTAMP
}
Database Schema Changes
Migration: Add Workflow Type Support
-- Migration: 20251210-add-workflow-type-support.ts
-- 1. Add workflow_type column to workflow_requests
ALTER TABLE workflow_requests
ADD COLUMN IF NOT EXISTS workflow_type VARCHAR(50) DEFAULT 'NON_TEMPLATIZED';
-- 2. Add template_id column (nullable, for admin templates)
ALTER TABLE workflow_requests
ADD COLUMN IF NOT EXISTS template_id UUID REFERENCES workflow_templates(template_id);
-- 3. Create index for workflow_type
CREATE INDEX IF NOT EXISTS idx_workflow_requests_workflow_type
ON workflow_requests(workflow_type);
-- 4. Create index for template_id
CREATE INDEX IF NOT EXISTS idx_workflow_requests_template_id
ON workflow_requests(template_id);
-- 5. Create dealer_claim_details table
CREATE TABLE IF NOT EXISTS dealer_claim_details (
claim_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
request_id UUID NOT NULL UNIQUE REFERENCES workflow_requests(request_id) ON DELETE CASCADE,
activity_name VARCHAR(500) NOT NULL,
activity_type VARCHAR(100) NOT NULL,
dealer_code VARCHAR(50) NOT NULL,
dealer_name VARCHAR(200) NOT NULL,
dealer_email VARCHAR(255),
dealer_phone VARCHAR(20),
dealer_address TEXT,
activity_date DATE,
location VARCHAR(255),
period_start_date DATE,
period_end_date DATE,
estimated_budget DECIMAL(15,2),
closed_expenses DECIMAL(15,2),
io_number VARCHAR(50),
io_available_balance DECIMAL(15,2),
io_blocked_amount DECIMAL(15,2),
io_remaining_balance DECIMAL(15,2),
sap_document_number VARCHAR(100),
dms_number VARCHAR(100),
e_invoice_number VARCHAR(100),
e_invoice_date DATE,
credit_note_number VARCHAR(100),
credit_note_date DATE,
credit_note_amount DECIMAL(15,2),
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_dealer_claim_details_request_id ON dealer_claim_details(request_id);
CREATE INDEX idx_dealer_claim_details_dealer_code ON dealer_claim_details(dealer_code);
-- 6. Create proposal_details table (Step 1: Dealer Proposal)
CREATE TABLE IF NOT EXISTS dealer_proposal_details (
proposal_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
request_id UUID NOT NULL REFERENCES workflow_requests(request_id) ON DELETE CASCADE,
proposal_document_path VARCHAR(500),
proposal_document_url VARCHAR(500),
cost_breakup JSONB, -- Array of {description, amount}
total_estimated_budget DECIMAL(15,2),
timeline_mode VARCHAR(10), -- 'date' | 'days'
expected_completion_date DATE,
expected_completion_days INTEGER,
dealer_comments TEXT,
submitted_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_dealer_proposal_details_request_id ON dealer_proposal_details(request_id);
-- 7. Create completion_documents table (Step 5: Dealer Completion)
CREATE TABLE IF NOT EXISTS dealer_completion_details (
completion_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
request_id UUID NOT NULL REFERENCES workflow_requests(request_id) ON DELETE CASCADE,
activity_completion_date DATE NOT NULL,
number_of_participants INTEGER,
closed_expenses JSONB, -- Array of {description, amount}
total_closed_expenses DECIMAL(15,2),
completion_documents JSONB, -- Array of document references
activity_photos JSONB, -- Array of photo references
submitted_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_dealer_completion_details_request_id ON dealer_completion_details(request_id);
Model Updates
1. Update WorkflowRequest Model
// Re_Backend/src/models/WorkflowRequest.ts
interface WorkflowRequestAttributes {
requestId: string;
requestNumber: string;
initiatorId: string;
templateType: 'CUSTOM' | 'TEMPLATE';
workflowType: string; // NEW: 'NON_TEMPLATIZED' | 'CLAIM_MANAGEMENT' | etc.
templateId?: string; // NEW: Reference to workflow_templates
title: string;
description: string;
// ... existing fields
}
// Add association
WorkflowRequest.hasOne(DealerClaimDetails, {
as: 'claimDetails',
foreignKey: 'requestId',
sourceKey: 'requestId'
});
2. Create DealerClaimDetails Model
// Re_Backend/src/models/DealerClaimDetails.ts
import { DataTypes, Model } from 'sequelize';
import { sequelize } from '@config/database';
import { WorkflowRequest } from './WorkflowRequest';
interface DealerClaimDetailsAttributes {
claimId: string;
requestId: string;
activityName: string;
activityType: string;
dealerCode: string;
dealerName: string;
// ... all claim-specific fields
}
class DealerClaimDetails extends Model<DealerClaimDetailsAttributes> {
public claimId!: string;
public requestId!: string;
// ... fields
}
DealerClaimDetails.init({
claimId: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
field: 'claim_id'
},
requestId: {
type: DataTypes.UUID,
allowNull: false,
unique: true,
field: 'request_id',
references: {
model: 'workflow_requests',
key: 'request_id'
}
},
// ... all other fields
}, {
sequelize,
modelName: 'DealerClaimDetails',
tableName: 'dealer_claim_details',
timestamps: true
});
// Association
DealerClaimDetails.belongsTo(WorkflowRequest, {
as: 'workflowRequest',
foreignKey: 'requestId',
targetKey: 'requestId'
});
export { DealerClaimDetails };
Service Layer Pattern
1. Template-Aware Service Factory
// Re_Backend/src/services/templateService.factory.ts
import { WorkflowRequest } from '../models/WorkflowRequest';
import { DealerClaimService } from './dealerClaim.service';
import { NonTemplatizedService } from './nonTemplatized.service';
export class TemplateServiceFactory {
static getService(workflowType: string) {
switch (workflowType) {
case 'CLAIM_MANAGEMENT':
return new DealerClaimService();
case 'NON_TEMPLATIZED':
return new NonTemplatizedService();
default:
// For future templates, use a generic service or throw error
throw new Error(`Unsupported workflow type: ${workflowType}`);
}
}
static async getRequestDetails(requestId: string) {
const request = await WorkflowRequest.findByPk(requestId);
if (!request) return null;
const service = this.getService(request.workflowType);
return service.getRequestDetails(request);
}
}
2. Unified Workflow Service (No Changes Needed)
The existing WorkflowService.listMyRequests() and listOpenForMe() methods will automatically include all request types because they query workflow_requests table without filtering by workflow_type.
// Existing code works as-is - no changes needed!
async listMyRequests(userId: string, page: number, limit: number, filters?: {...}) {
// This query automatically includes ALL workflow types
const requests = await WorkflowRequest.findAll({
where: {
initiatorId: userId,
isDraft: false,
// ... filters
// NO workflow_type filter - includes everything!
}
});
return requests;
}
API Endpoints
1. Create Claim Management Request
// Re_Backend/src/controllers/dealerClaim.controller.ts
async createClaimRequest(req: AuthenticatedRequest, res: Response) {
const userId = req.user?.userId;
const {
activityName,
activityType,
dealerCode,
// ... claim-specific fields
} = req.body;
// 1. Create workflow request (common)
const workflowRequest = await WorkflowRequest.create({
initiatorId: userId,
templateType: 'CUSTOM',
workflowType: 'CLAIM_MANAGEMENT', // Identify as claim
title: `${activityName} - Claim Request`,
description: req.body.requestDescription,
totalLevels: 8, // Fixed 8-step workflow
// ... other common fields
});
// 2. Create claim-specific details
const claimDetails = await DealerClaimDetails.create({
requestId: workflowRequest.requestId,
activityName,
activityType,
dealerCode,
// ... claim-specific fields
});
// 3. Create approval levels (8 steps)
await this.createClaimApprovalLevels(workflowRequest.requestId);
return ResponseHandler.success(res, {
request: workflowRequest,
claimDetails
});
}
2. Get Request Details (Template-Aware)
async getRequestDetails(req: Request, res: Response) {
const { requestId } = req.params;
const request = await WorkflowRequest.findByPk(requestId, {
include: [
{ model: User, as: 'initiator' },
// Conditionally include template-specific data
...(request.workflowType === 'CLAIM_MANAGEMENT'
? [{ model: DealerClaimDetails, as: 'claimDetails' }]
: [])
]
});
// Use factory to get template-specific service
const templateService = TemplateServiceFactory.getService(request.workflowType);
const enrichedDetails = await templateService.enrichRequestDetails(request);
return ResponseHandler.success(res, enrichedDetails);
}
Frontend Integration
1. Request List Views (No Changes Needed)
The existing "My Requests" and "Open Requests" pages will automatically show all request types because the backend doesn't filter by workflow_type.
// Frontend: MyRequests.tsx - No changes needed!
const fetchMyRequests = async () => {
const result = await workflowApi.listMyInitiatedWorkflows({
page,
limit: itemsPerPage
});
// Returns ALL request types automatically
};
2. Request Detail Page (Template-Aware Rendering)
// Frontend: RequestDetail.tsx
const RequestDetail = ({ requestId }) => {
const request = useRequestDetails(requestId);
// Render based on workflow type
if (request.workflowType === 'CLAIM_MANAGEMENT') {
return <ClaimManagementDetail request={request} />;
} else if (request.workflowType === 'NON_TEMPLATIZED') {
return <NonTemplatizedDetail request={request} />;
} else {
// Future templates - use generic renderer or template config
return <GenericWorkflowDetail request={request} />;
}
};
Adding New Templates (Future)
Step 1: Admin Creates Template in UI
- Admin goes to "Template Management" page
- Creates new template with:
- Template name: "Vendor Payment"
- Template code: "VENDOR_PAYMENT"
- Approval levels configuration
- Form fields configuration
Step 2: Database Entry Created
INSERT INTO workflow_templates (
template_name,
template_code,
workflow_type,
approval_levels_config,
form_fields_config,
is_active,
is_system_template
) VALUES (
'Vendor Payment',
'VENDOR_PAYMENT',
'VENDOR_PAYMENT',
'{"levels": [...], "tat": {...}}'::jsonb,
'{"fields": [...]}'::jsonb,
true,
false -- Admin-created, not system template
);
Step 3: Create Extension Table (If Needed)
CREATE TABLE vendor_payment_details (
payment_id UUID PRIMARY KEY,
request_id UUID UNIQUE REFERENCES workflow_requests(request_id),
vendor_code VARCHAR(50),
invoice_number VARCHAR(100),
payment_amount DECIMAL(15,2),
-- ... vendor-specific fields
);
Step 4: Create Service (Optional - Can Use Generic Service)
// Re_Backend/src/services/vendorPayment.service.ts
export class VendorPaymentService {
async getRequestDetails(request: WorkflowRequest) {
const paymentDetails = await VendorPaymentDetails.findOne({
where: { requestId: request.requestId }
});
return {
...request.toJSON(),
paymentDetails
};
}
}
// Update factory
TemplateServiceFactory.getService(workflowType: string) {
switch (workflowType) {
case 'VENDOR_PAYMENT':
return new VendorPaymentService();
// ... existing cases
}
}
Step 5: Frontend Component (Optional)
// Frontend: components/VendorPaymentDetail.tsx
export function VendorPaymentDetail({ request }) {
// Render vendor payment specific UI
}
Benefits of This Architecture
- Unified Data Model: All requests in one table, easy to query
- Automatic Inclusion: My Requests/Open Requests show all types automatically
- Extensibility: Add new templates without modifying existing code
- Type Safety: Template-specific data in separate tables
- Flexibility: Support both system templates and admin-created templates
- Backward Compatible: Existing non-templatized requests continue to work
Migration Strategy
- Phase 1: Add
workflow_typecolumn, set default to 'NON_TEMPLATIZED' for existing requests - Phase 2: Create
dealer_claim_detailstable and models - Phase 3: Update claim management creation flow to use new structure
- Phase 4: Update request detail endpoints to be template-aware
- Phase 5: Frontend updates (if needed) for template-specific rendering
Summary
- All requests use
workflow_requeststable - Template identification via
workflow_typefield - Template-specific data in extension tables (e.g.,
dealer_claim_details) - Unified views automatically include all types
- Future templates can be added by admins without code changes
- Existing functionality remains unchanged
This architecture ensures that:
- ✅ Claim Management requests appear in My Requests/Open Requests
- ✅ Non-templatized requests continue to work
- ✅ Future templates can be added easily
- ✅ No code duplication
- ✅ Single source of truth for all requests