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:
{
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:
- If all data present → Auto-create IMSS registration task
- 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:
{
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:
- Process IMSS de-registration (baja)
- Calculate final settlement
- Generate finiquito document
- Queue payroll stamp for settlement payment
salary_change.approve
Triggered when a salary modification is approved.
Payload:
{
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:
- Update IMSS salary (modificacion salarial)
- Adjust payroll calculations from effective date
- Update tax bracket if applicable
expense.approve
Triggered when an expense reimbursement is approved.
Payload:
{
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:
- Record expense in accounting
- Queue for reimbursement in next payroll
- Match with CFDI receipts if UUIDs provided
vacation.approve
Triggered when vacation request is approved.
Payload:
{
eventType: "vacation.approve",
data: {
decisionId: string;
employeeId: string;
startDate: string;
endDate: string;
days: number;
isPaid: boolean;
approvedBy: string;
approvedAt: string;
}
}Constanza Actions:
- Update vacation balance tracking
- Adjust payroll if paid vacation
- Note absence for attendance records
Pending Tasks System
When decision events arrive with incomplete data, Constanza creates pending tasks.
Task Structure
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
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:
// 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:
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:
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:
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:
// 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:
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.