584 lines
17 KiB
Markdown
584 lines
17 KiB
Markdown
# 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:
|
|
1. **Unified Request System**: All requests (templatized, non-templatized, claim management) use the same `workflow_requests` table
|
|
2. **Template Identification**: Distinguish between different workflow types
|
|
3. **Extensibility**: Easy addition of new templates by admins without code changes
|
|
4. **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:
|
|
|
|
```sql
|
|
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 to `workflow_templates` table if using an admin-created template
|
|
|
|
### 2. **Template-Specific Data Storage**
|
|
|
|
Each workflow type can have its own extension table for type-specific data:
|
|
|
|
```sql
|
|
-- 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)**
|
|
|
|
```sql
|
|
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
|
|
|
|
```sql
|
|
-- 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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`.
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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)
|
|
|
|
```typescript
|
|
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`.
|
|
|
|
```typescript
|
|
// 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)
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
1. Admin goes to "Template Management" page
|
|
2. Creates new template with:
|
|
- Template name: "Vendor Payment"
|
|
- Template code: "VENDOR_PAYMENT"
|
|
- Approval levels configuration
|
|
- Form fields configuration
|
|
|
|
### Step 2: Database Entry Created
|
|
|
|
```sql
|
|
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)
|
|
|
|
```sql
|
|
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)
|
|
|
|
```typescript
|
|
// 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)
|
|
|
|
```typescript
|
|
// Frontend: components/VendorPaymentDetail.tsx
|
|
|
|
export function VendorPaymentDetail({ request }) {
|
|
// Render vendor payment specific UI
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Benefits of This Architecture
|
|
|
|
1. **Unified Data Model**: All requests in one table, easy to query
|
|
2. **Automatic Inclusion**: My Requests/Open Requests show all types automatically
|
|
3. **Extensibility**: Add new templates without modifying existing code
|
|
4. **Type Safety**: Template-specific data in separate tables
|
|
5. **Flexibility**: Support both system templates and admin-created templates
|
|
6. **Backward Compatible**: Existing non-templatized requests continue to work
|
|
|
|
---
|
|
|
|
## Migration Strategy
|
|
|
|
1. **Phase 1**: Add `workflow_type` column, set default to 'NON_TEMPLATIZED' for existing requests
|
|
2. **Phase 2**: Create `dealer_claim_details` table and models
|
|
3. **Phase 3**: Update claim management creation flow to use new structure
|
|
4. **Phase 4**: Update request detail endpoints to be template-aware
|
|
5. **Phase 5**: Frontend updates (if needed) for template-specific rendering
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
- **All requests** use `workflow_requests` table
|
|
- **Template identification** via `workflow_type` field
|
|
- **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
|
|
|