← back to Norma

app/api/email-analyzer/learnings/route.ts

147 lines

/**
 * Cross-element learnings journal.
 *
 *   GET    /api/email-analyzer/learnings?element_type=subject&session_id=xxx
 *   POST   /api/email-analyzer/learnings — { element_type, element_id, snippet, learning, tags, session_id, version_id }
 *   PATCH  /api/email-analyzer/learnings — { id, learning?, tags? }
 *   DELETE /api/email-analyzer/learnings?id=xxx
 *
 * Use cases:
 *   - Note why a subject line worked / didn't
 *   - Capture a reusable rule ("short subjects beat long for fundraising")
 *   - Pin a rewrite instruction that consistently improves scores
 */

import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';

export async function GET(request: NextRequest) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  try {
    const sp = new URL(request.url).searchParams;
    const elementType = sp.get('element_type');
    const sessionId = sp.get('session_id');
    const conditions: string[] = [];
    const params: unknown[] = [];
    if (elementType) {
      params.push(elementType);
      conditions.push(`element_type = $${params.length}`);
    }
    if (sessionId) {
      params.push(sessionId);
      conditions.push(`session_id = $${params.length}`);
    }
    const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';

    const { rows } = await query(
      `SELECT id, session_id, version_id, element_type, element_id,
              snippet, learning, tags, created_at
         FROM email_analyzer_learnings
         ${where}
         ORDER BY created_at DESC
         LIMIT 200`,
      params,
    );
    return NextResponse.json({ learnings: rows });
  } catch (err) {
    console.error('[learnings] GET error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to load learnings' }, { status: 500 });
  }
}

export async function POST(request: NextRequest) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  try {
    const body = await request.json();
    const {
      session_id,
      version_id,
      element_type,
      element_id,
      snippet,
      learning,
      tags,
    } = body;
    if (!element_type || !learning) {
      return NextResponse.json({ error: 'element_type and learning are required' }, { status: 400 });
    }
    const { rows } = await query(
      `INSERT INTO email_analyzer_learnings
         (session_id, version_id, element_type, element_id, snippet, learning, tags)
       VALUES ($1, $2, $3, $4, $5, $6, $7)
       RETURNING *`,
      [
        session_id || null,
        version_id || null,
        element_type,
        element_id || null,
        snippet || null,
        learning,
        Array.isArray(tags) ? tags.slice(0, 20) : [],
      ],
    );
    return NextResponse.json({ learning: rows[0] }, { status: 201 });
  } catch (err) {
    console.error('[learnings] POST error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to save learning' }, { status: 500 });
  }
}

export async function PATCH(request: NextRequest) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  try {
    const { id, learning, tags } = await request.json();
    if (!id) return NextResponse.json({ error: 'id required' }, { status: 400 });
    const sets: string[] = [];
    const params: unknown[] = [];
    if (learning !== undefined) {
      params.push(learning);
      sets.push(`learning = $${params.length}`);
    }
    if (tags !== undefined) {
      params.push(Array.isArray(tags) ? tags.slice(0, 20) : []);
      sets.push(`tags = $${params.length}`);
    }
    if (sets.length === 0) {
      return NextResponse.json({ error: 'nothing to update' }, { status: 400 });
    }
    params.push(id);
    const { rows, rowCount } = await query(
      `UPDATE email_analyzer_learnings SET ${sets.join(', ')}
        WHERE id = $${params.length} RETURNING *`,
      params,
    );
    if (!rowCount) return NextResponse.json({ error: 'not found' }, { status: 404 });
    return NextResponse.json({ learning: rows[0] });
  } catch (err) {
    console.error('[learnings] PATCH error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to update' }, { status: 500 });
  }
}

export async function DELETE(request: NextRequest) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  try {
    const id = new URL(request.url).searchParams.get('id');
    if (!id) return NextResponse.json({ error: 'id required' }, { status: 400 });
    const { rowCount } = await query(
      `DELETE FROM email_analyzer_learnings WHERE id = $1`,
      [id],
    );
    if (!rowCount) return NextResponse.json({ error: 'not found' }, { status: 404 });
    return NextResponse.json({ deleted: true, id });
  } catch (err) {
    console.error('[learnings] DELETE error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to delete' }, { status: 500 });
  }
}