/** * applyOps – execute a fenced ```ops``` block returned by Jess. * Supports milestones, tasks, impacts, scenario utilities, and college profile. * * @param {object} opsObj – parsed JSON inside ```ops``` * @param {object} req – Express request (for auth header) * @param {string} scenarioId – current career_profile_id (optional but lets us * auto-fill when the bot forgets) * @return {string[]} – human-readable confirmations */ export async function applyOps(opsObj = {}, req, scenarioId = null) { if (!Array.isArray(opsObj?.milestones) && !Array.isArray(opsObj?.tasks) && !Array.isArray(opsObj?.impacts) && !Array.isArray(opsObj?.scenarios) && !opsObj.collegeProfile) return []; const apiBase = process.env.APTIVA_INTERNAL_API || 'http://localhost:5002/api'; const auth = (p, o = {}) => internalFetch(req, `${apiBase}${p}`, { headers: { 'Content-Type': 'application/json', ...(o.headers || {}) }, ...o }); const confirmations = []; /* ──────────────────────────────────────────────────── 1. MILESTONE-LEVEL OPS (unchanged behaviour) ──────────────────────────────────────────────────── */ for (const m of opsObj.milestones || []) { const op = (m?.op || '').toUpperCase(); if (op === 'DELETE' && m.id) { // single-scenario delete const r = await auth(`/premium/milestones/${m.id.trim()}`, { method: 'DELETE' }); if (r.ok) confirmations.push(`Deleted milestone ${m.id}`); continue; } if (op === 'UPDATE' && m.id && m.patch) { const r = await auth(`/premium/milestones/${m.id}`, { method: 'PUT', body: JSON.stringify(m.patch) }); if (r.ok) confirmations.push(`Updated milestone ${m.id}`); continue; } if (op === 'CREATE' && m.data) { m.data.career_profile_id = m.data.career_profile_id || scenarioId; const r = await auth('/premium/milestone', { method: 'POST', body: JSON.stringify(m.data) }); if (r.ok) { const j = await r.json(); const newId = Array.isArray(j) ? j[0]?.id : j.id; confirmations.push(`Created milestone ${newId || '(new)'}`); } continue; } if (op === 'DELETEALL' && m.id) { // delete across every scenario const r = await auth(`/premium/milestones/${m.id}/all`, { method: 'DELETE' }); if (r.ok) confirmations.push(`Deleted milestone ${m.id} from all scenarios`); continue; } if (op === 'COPY' && m.id && Array.isArray(m.targetScenarioIds)) { const r = await auth('/premium/milestone/copy', { method: 'POST', body : JSON.stringify({ milestoneId: m.id, scenarioIds: m.targetScenarioIds }) }); if (r.ok) confirmations.push(`Copied milestone ${m.id} → ${m.targetScenarioIds.length} scenario(s)`); continue; } } /* ──────────────────────────────────────────────────── 2. TASK-LEVEL OPS ──────────────────────────────────────────────────── */ for (const t of opsObj.tasks || []) { const op = (t?.op || '').toUpperCase(); if (op === 'CREATE' && t.data && t.data.milestone_id) { await auth('/premium/tasks', { method: 'POST', body: JSON.stringify(t.data) }); confirmations.push(`Added task to milestone ${t.data.milestone_id}`); continue; } if (op === 'UPDATE' && t.taskId && t.patch) { await auth(`/premium/tasks/${t.taskId}`, { method: 'PUT', body: JSON.stringify(t.patch) }); confirmations.push(`Updated task ${t.taskId}`); continue; } if (op === 'DELETE' && t.taskId) { await auth(`/premium/tasks/${t.taskId}`, { method: 'DELETE' }); confirmations.push(`Deleted task ${t.taskId}`); continue; } } /* ──────────────────────────────────────────────────── 3. IMPACT-LEVEL OPS ──────────────────────────────────────────────────── */ for (const imp of opsObj.impacts || []) { const op = (imp?.op || '').toUpperCase(); if (op === 'CREATE' && imp.data && imp.data.milestone_id) { await auth('/premium/milestone-impacts', { method: 'POST', body: JSON.stringify(imp.data) }); confirmations.push(`Added impact to milestone ${imp.data.milestone_id}`); continue; } if (op === 'UPDATE' && imp.impactId && imp.patch) { await auth(`/premium/milestone-impacts/${imp.impactId}`, { method: 'PUT', body: JSON.stringify(imp.patch) }); confirmations.push(`Updated impact ${imp.impactId}`); continue; } if (op === 'DELETE' && imp.impactId) { await auth(`/premium/milestone-impacts/${imp.impactId}`, { method: 'DELETE' }); confirmations.push(`Deleted impact ${imp.impactId}`); continue; } } /* ──────────────────────────────────────────────────── 4. SCENARIO (career_profile) OPS ──────────────────────────────────────────────────── */ for (const s of opsObj.scenarios || []) { const op = (s?.op || '').toUpperCase(); if (op === 'CREATE' && s.data?.career_name) { await auth('/premium/career-profile', { method: 'POST', body: JSON.stringify(s.data) }); confirmations.push(`Created scenario “${s.data.career_name}”`); continue; } if (op === 'UPDATE' && s.scenarioId && s.patch) { /* if only goals are patched, hit the goals route; otherwise create a PUT route */ const hasOnlyGoals = Object.keys(s.patch).length === 1 && s.patch.career_goals !== undefined; const url = hasOnlyGoals ? `/premium/career-profile/${s.scenarioId}/goals` : `/premium/career-profile`; // <-- add generic PATCH if you implemented one await auth(url.replace(/\/$/, `/${s.scenarioId}`), { method: 'PUT', body: JSON.stringify(s.patch) }); confirmations.push(`Updated scenario ${s.scenarioId}`); continue; } if (op === 'DELETE' && s.scenarioId) { await auth(`/premium/career-profile/${s.scenarioId}`, { method: 'DELETE' }); confirmations.push(`Deleted scenario ${s.scenarioId}`); continue; } if (op === 'CLONE' && s.sourceId) { await auth('/premium/career-profile/clone', { method: 'POST', body: JSON.stringify({ sourceId : s.sourceId, overrides : s.overrides || {} })}); confirmations.push(`Cloned scenario ${s.sourceId}`); continue; } } /* ──────────────────────────────────────────────────── 5. COLLEGE PROFILE (single op per block) ──────────────────────────────────────────────────── */ if (opsObj.collegeProfile?.op?.toUpperCase() === 'UPSERT' && opsObj.collegeProfile.data) { await auth('/premium/college-profile', { method: 'POST', body: JSON.stringify(opsObj.collegeProfile.data) }); confirmations.push('Saved college profile'); } return confirmations; }