Skip to content

Decision Event Architecture

Status: IMPLEMENTED (Colectiva → Constanza) Pattern: Approval-driven cross-platform coordination


Overview

Decision events are a special category of webhooks that represent approved business decisions from Colectiva. These decisions trigger coordinated actions across the ecosystem.

Unlike regular entity webhooks (employee.created, etc.), decision events:

  • Represent finalized approvals, not just data changes
  • Often require action from multiple platforms
  • May contain incomplete data requiring user intervention
  • Create audit trails for compliance

Decision Flow

┌─────────────────────────────────────────────────────────────────┐
│                         COLECTIVA                               │
│                                                                 │
│  1. Manager submits decision request (hire, terminate, etc.)    │
│  2. Approval workflow runs (may have multiple approvers)        │
│  3. Decision APPROVED → Webhook fired                           │
└─────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│                         CONSTANZA                               │
│                                                                 │
│  4. Receive webhook, verify signature                           │
│  5. Check if all required data is present                       │
│  6a. Complete → Process automatically                           │
│  6b. Incomplete → Create pending task for accountant            │
└─────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│                         CAMINO                                  │
│                                                                 │
│  7. Send notifications (email, WhatsApp) about decision         │
│  8. Update CRM records if applicable                            │
└─────────────────────────────────────────────────────────────────┘

Decision Types

hiring.approve

Triggered when a new hire is approved.

Required for IMSS registration:

  • NSS (Social Security Number)
  • CURP
  • RFC
  • Salary
  • Start date
  • Contract type

Payload:

typescript
{
  eventType: "hiring.approve",
  data: {
    decisionId: string;
    employeeId: string;
    employee: {
      name: string;
      email: string;
      curp?: string;
      rfc?: string;
      nss?: string;
      // ... other fields
    };
    position: {
      title: string;
      department: string;
      salary: number;
      salaryType: "monthly" | "daily";
    };
    startDate: string;
    contractType: "indefinido" | "temporal" | "capacitacion";
    approvedBy: string;
    approvedAt: string;
  }
}

Constanza Actions:

  1. If all data present → Auto-create IMSS registration task
  2. If missing NSS/CURP/RFC → Create pending task for accountant to complete

termination.approve

Triggered when employee termination is approved.

Required for IMSS de-registration:

  • Employee ID (must already be registered)
  • Termination date
  • Termination reason
  • Settlement calculation

Payload:

typescript
{
  eventType: "termination.approve",
  data: {
    decisionId: string;
    employeeId: string;
    employee: {
      name: string;
      nss: string;
    };
    terminationDate: string;
    reason: "renuncia" | "despido" | "mutuo_acuerdo" | "termino_contrato";
    settlement: {
      severance?: number;
      vacationDays: number;
      aguinaldoDays: number;
      // calculated amounts
    };
    approvedBy: string;
    approvedAt: string;
  }
}

Constanza Actions:

  1. Process IMSS de-registration (baja)
  2. Calculate final settlement
  3. Generate finiquito document
  4. Queue payroll stamp for settlement payment

salary_change.approve

Triggered when a salary modification is approved.

Payload:

typescript
{
  eventType: "salary_change.approve",
  data: {
    decisionId: string;
    employeeId: string;
    employee: {
      name: string;
      nss: string;
    };
    previousSalary: number;
    newSalary: number;
    effectiveDate: string;
    reason: string;
    approvedBy: string;
    approvedAt: string;
  }
}

Constanza Actions:

  1. Update IMSS salary (modificacion salarial)
  2. Adjust payroll calculations from effective date
  3. Update tax bracket if applicable

expense.approve

Triggered when an expense reimbursement is approved.

Payload:

typescript
{
  eventType: "expense.approve",
  data: {
    decisionId: string;
    expenseId: string;
    employeeId: string;
    amount: number;
    currency: string;
    category: string;
    description: string;
    receipts: Array<{
      url: string;
      uuid?: string;  // CFDI UUID if available
    }>;
    approvedBy: string;
    approvedAt: string;
  }
}

Constanza Actions:

  1. Record expense in accounting
  2. Queue for reimbursement in next payroll
  3. Match with CFDI receipts if UUIDs provided

vacation.approve

Triggered when vacation request is approved.

Payload:

typescript
{
  eventType: "vacation.approve",
  data: {
    decisionId: string;
    employeeId: string;
    startDate: string;
    endDate: string;
    days: number;
    isPaid: boolean;
    approvedBy: string;
    approvedAt: string;
  }
}

Constanza Actions:

  1. Update vacation balance tracking
  2. Adjust payroll if paid vacation
  3. Note absence for attendance records

Pending Tasks System

When decision events arrive with incomplete data, Constanza creates pending tasks.

Task Structure

typescript
interface PendingTask {
  id: string;
  organizationId: string;

  // Source tracking
  sourceEvent: {
    type: string;          // e.g., "hiring.approve"
    id: string;            // Decision ID from Colectiva
    receivedAt: Date;
  };

  // Task details
  taskType: "complete_data" | "review" | "manual_action";
  title: string;
  description: string;

  // What's missing
  requiredFields: string[];
  currentData: Record<string, any>;

  // Assignment
  assignedTo?: string;     // User ID
  assignedRole: string;    // e.g., "accountant"

  // Status
  status: "pending" | "in_progress" | "completed" | "cancelled";
  priority: "low" | "medium" | "high" | "urgent";
  dueDate?: Date;

  // Completion
  completedBy?: string;
  completedAt?: Date;
  completionData?: Record<string, any>;
}

UI for Pending Tasks

Constanza displays pending tasks in dashboard:

┌─────────────────────────────────────────────────────────────────┐
│  Tareas Pendientes del Ecosistema                    Ver todas →│
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ⚠️ Alta - Completar datos de nuevo empleado                    │
│     Juan Pérez - Faltan: NSS, CURP                             │
│     Desde: Colectiva · hace 2 horas                            │
│     [Completar Datos]                                          │
│                                                                 │
│  🔵 Media - Revisar baja de empleado                           │
│     María García - Verificar cálculo de finiquito              │
│     Desde: Colectiva · hace 1 día                              │
│     [Revisar]                                                  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Implementation Details

Constanza Webhook Handler

Location: src/routes/api/ecosystem/webhooks/+server.ts

typescript
const decisionHandlers = {
  'hiring.approve': async (data: HiringDecision) => {
    const requiredFields = ['nss', 'curp', 'rfc'];
    const missingFields = requiredFields.filter(f => !data.employee[f]);

    if (missingFields.length > 0) {
      // Create pending task
      await PendingTaskService.create({
        organizationId: data.organizationId,
        sourceEvent: {
          type: 'hiring.approve',
          id: data.decisionId,
          receivedAt: new Date()
        },
        taskType: 'complete_data',
        title: `Completar datos: ${data.employee.name}`,
        description: `Nueva contratación aprobada. Faltan datos para registro IMSS.`,
        requiredFields: missingFields,
        currentData: data.employee,
        assignedRole: 'accountant',
        priority: 'high'
      });
    } else {
      // Auto-process
      await IMSSService.registerEmployee(data);
    }
  },

  'termination.approve': async (data: TerminationDecision) => {
    // Similar pattern...
  }
};

Bidirectional Communication

After Constanza completes a pending task, it can notify Colectiva:

typescript
// When task is completed
async function onTaskCompleted(task: PendingTask) {
  // Update local records
  await processCompletedTask(task);

  // Notify source platform
  await WebhookService.send('colectiva', {
    eventType: 'task.completed',
    data: {
      originalDecisionId: task.sourceEvent.id,
      taskId: task.id,
      completedBy: task.completedBy,
      completedAt: task.completedAt,
      resultData: task.completionData
    }
  });
}

Error Handling

Missing Required Fields

Don't fail silently. Create actionable tasks:

typescript
if (!employee.nss) {
  await createTask({
    title: "NSS requerido para alta IMSS",
    description: `El empleado ${employee.name} no tiene NSS registrado.`,
    requiredFields: ['nss'],
    priority: 'high'
  });
}

Invalid Data

Log and create review task:

typescript
if (!isValidCURP(employee.curp)) {
  await createTask({
    taskType: 'review',
    title: "CURP inválido",
    description: `El CURP "${employee.curp}" no tiene formato válido.`,
    priority: 'high'
  });
}

External Service Failures

Retry with backoff, then create manual task:

typescript
try {
  await IMSSService.register(employee);
} catch (err) {
  if (retries < 3) {
    await queueRetry(employee, retries + 1);
  } else {
    await createTask({
      taskType: 'manual_action',
      title: "Registro IMSS manual requerido",
      description: `Error de conexión con IMSS después de múltiples intentos.`,
      priority: 'urgent'
    });
  }
}

Audit Trail

All decision events are logged for compliance:

typescript
// Firestore: organizations/{orgId}/decisionAudit/{eventId}
{
  eventId: string;
  eventType: string;
  receivedAt: Timestamp;
  processedAt: Timestamp;
  status: "auto_processed" | "pending_task" | "failed";
  taskId?: string;  // If pending task created
  result?: any;

  // Original payload (encrypted)
  encryptedPayload: string;
}

Testing

Use Colectiva's test mode to send simulated decisions:

bash
curl -X POST https://colectiva.redbroomsoftware.com/api/test/simulate-decision \
  -H "Authorization: Bearer $TEST_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "eventType": "hiring.approve",
    "targetApp": "constanza",
    "data": {
      "employee": {
        "name": "Test Employee",
        "email": "test@example.com"
        // Intentionally missing NSS to trigger pending task
      }
    }
  }'

Decision events form the backbone of ecosystem coordination. Handle them carefully.

Red Broom Software Ecosystem