Re_Backend/docs/EXTENSIBLE_WORKFLOW_ARCHITECTURE.md

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:

  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:

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:

-- 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

  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

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

  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