Skip to content

Commit 826d080

Browse files
committed
feat: server-side automation evaluation in handleCreateApplication
- Evaluate active 'app_received' automation rules server-side after application creation - Automations now fire even when no user has the ATS dashboard open - Supports move_stage, shortlist, reject_candidate actions with real DB mutations - Logs execution to automation_logs with [SERVER] prefix for traceability - Updates rule run_count and last_run_at on each execution - Gracefully skips email actions already handled by the built-in email flow
1 parent 2600731 commit 826d080

1 file changed

Lines changed: 99 additions & 0 deletions

File tree

backend/simpatico-ats.js

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3509,6 +3509,105 @@ Return ONLY valid JSON in format: {"match_score": 85, "reason": "Brief 1-sentenc
35093509
auto_rejected: autoRejected,
35103510
match_score,
35113511
});
3512+
3513+
// ═══════════════════════════════════════════════════════════════
3514+
// 7. SERVER-SIDE AUTOMATION: Evaluate 'app_received' rules
3515+
// This ensures automations fire even when no user has the
3516+
// ATS dashboard open (e.g., candidates applying via careers page)
3517+
// ═══════════════════════════════════════════════════════════════
3518+
try {
3519+
const rulesRes = await sbFetch(
3520+
env,
3521+
"GET",
3522+
`/rest/v1/automation_rules?enabled=eq.true&trigger=eq.app_received&module=eq.ats&select=*`,
3523+
null,
3524+
false,
3525+
ctx.tenantId,
3526+
);
3527+
const rules = await rulesRes.json();
3528+
3529+
if (Array.isArray(rules) && rules.length > 0) {
3530+
console.log(`[ATS-AUTO] Evaluating ${rules.length} app_received rule(s) for tenant ${ctx.tenantId}`);
3531+
3532+
for (const rule of rules) {
3533+
try {
3534+
// Check conditions
3535+
if (rule.cond_score && match_score !== null && match_score < parseInt(rule.cond_score)) {
3536+
continue; // Score below threshold, skip
3537+
}
3538+
if (rule.cond_stage && status !== rule.cond_stage) {
3539+
continue; // Stage mismatch, skip
3540+
}
3541+
3542+
const actions = rule.actions || [];
3543+
const results = [];
3544+
3545+
for (const action of actions) {
3546+
switch (action.type) {
3547+
case 'send_email':
3548+
// Email is already handled by the auto-shortlist/reject/confirmation flow above
3549+
results.push({ type: 'send_email', status: 'skipped', detail: 'Handled by built-in email flow' });
3550+
break;
3551+
3552+
case 'move_stage':
3553+
if (action.target && action.target !== status) {
3554+
await sbFetch(env, "PATCH", `/rest/v1/job_applications?id=eq.${app.id}`, { status: action.target }, false, ctx.tenantId);
3555+
results.push({ type: 'move_stage', status: 'success', detail: `Moved to ${action.target}` });
3556+
} else {
3557+
results.push({ type: 'move_stage', status: 'skipped', detail: 'Already in target stage' });
3558+
}
3559+
break;
3560+
3561+
case 'shortlist':
3562+
if (status !== 'screening') {
3563+
await sbFetch(env, "PATCH", `/rest/v1/job_applications?id=eq.${app.id}`, { status: 'screening' }, false, ctx.tenantId);
3564+
results.push({ type: 'shortlist', status: 'success', detail: 'Moved to screening' });
3565+
}
3566+
break;
3567+
3568+
case 'reject_candidate':
3569+
if (status !== 'rejected') {
3570+
await sbFetch(env, "PATCH", `/rest/v1/job_applications?id=eq.${app.id}`, { status: 'rejected' }, false, ctx.tenantId);
3571+
results.push({ type: 'reject_candidate', status: 'success', detail: 'Auto-rejected by rule' });
3572+
}
3573+
break;
3574+
3575+
default:
3576+
results.push({ type: action.type, status: 'dispatched', detail: action.msg || action.type });
3577+
}
3578+
}
3579+
3580+
// Log execution
3581+
const logEntry = {
3582+
rule_name: rule.name,
3583+
trigger: 'app_received',
3584+
module: 'ats',
3585+
status: 'success',
3586+
detail: `[SERVER] ${results.map(r => r.type + ':' + r.status).join(', ')}`,
3587+
target_id: app.id,
3588+
action_taken: results.map(r => r.detail).join(' | '),
3589+
created_at: new Date().toISOString(),
3590+
ts: new Date().toISOString(),
3591+
tenant_id: ctx.tenantId,
3592+
};
3593+
await sbFetch(env, "POST", "/rest/v1/automation_logs", logEntry, false, ctx.tenantId);
3594+
3595+
// Update run count
3596+
await sbFetch(env, "PATCH", `/rest/v1/automation_rules?id=eq.${rule.id}`, {
3597+
run_count: (rule.run_count || 0) + 1,
3598+
last_run_at: new Date().toISOString(),
3599+
}, false, ctx.tenantId);
3600+
3601+
console.log(`[ATS-AUTO] Rule "${rule.name}" executed for application ${app.id}`);
3602+
} catch (ruleErr) {
3603+
console.warn(`[ATS-AUTO] Rule "${rule.name}" failed:`, ruleErr.message);
3604+
}
3605+
}
3606+
}
3607+
} catch (autoErr) {
3608+
console.warn("[ATS-AUTO] Server-side automation evaluation failed:", autoErr.message);
3609+
}
3610+
35123611
return apiResponse(
35133612
{ application: app, auto_scheduled: autoInterview, auto_rejected: autoRejected, match_score },
35143613
HTTP.CREATED,

0 commit comments

Comments
 (0)