When the assistant stages several verifikat and the user approves only some,
the rest lingered as pending_operations in Granskning until the 30-day expiry —
manual per-item cleanup. Now:
- lib/agent/pending/reject-conversation-pending.ts rejects a conversation's
still-pending proposals in one update (guarded on status='pending' so it never
stamps over a committed verifikat; company-scoped; keyed on the conversation).
- POST /api/agent/conversations/[id]/reject-pending — the chat's "Rensa förslag
som inte godkänts" button (appears when the thread has staged proposals; drops
the cards from view).
- Auto-clear on archive: archiving a thread ("I'm done") clears its leftover
proposals in the PATCH, best-effort.
Kept durable-by-default (proposals still come back on resume) — the button is
explicit user intent, not an auto-reject on every panel close, so resume still
works. 10 tests (helper + endpoint 401/404/404/happy); lint + guards + scoped
typecheck clean. UI button awaits founder visual sign-off.
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
51 lines
1.7 KiB
TypeScript
51 lines
1.7 KiB
TypeScript
import { NextResponse } from 'next/server'
|
|
import { withRouteContext } from '@/lib/api/with-route-context'
|
|
import { rejectPendingForConversation } from '@/lib/agent/pending/reject-conversation-pending'
|
|
|
|
// POST /api/agent/conversations/[id]/reject-pending
|
|
//
|
|
// Rejects every still-pending proposal the assistant staged in this
|
|
// conversation — the chat's "clear the proposals I didn't approve" action, so
|
|
// users don't have to reject leftovers one by one in Granskning.
|
|
|
|
const notFound = () =>
|
|
NextResponse.json(
|
|
{
|
|
error: {
|
|
code: 'CONVERSATION_NOT_FOUND',
|
|
message: 'Konversationen hittades inte.',
|
|
message_en: 'Conversation not found.',
|
|
},
|
|
},
|
|
{ status: 404 },
|
|
)
|
|
|
|
export const POST = withRouteContext(
|
|
'agent.conversations.reject_pending',
|
|
async (_request, ctx, { params }: { params: Promise<{ id: string }> }) => {
|
|
const { id } = await params
|
|
const { supabase, user } = ctx
|
|
|
|
// Conversations are user-scoped: prove ownership before touching its rows.
|
|
const { data: conv } = await supabase
|
|
.from('agent_conversations')
|
|
.select('id, company_id, user_id')
|
|
.eq('id', id)
|
|
.eq('user_id', user.id)
|
|
.maybeSingle()
|
|
if (!conv) return notFound()
|
|
|
|
// Defense in depth alongside RLS: still a member of the conversation's company.
|
|
const { data: membership } = await supabase
|
|
.from('company_members')
|
|
.select('role')
|
|
.eq('company_id', conv.company_id)
|
|
.eq('user_id', user.id)
|
|
.maybeSingle()
|
|
if (!membership) return notFound()
|
|
|
|
const cleared = await rejectPendingForConversation(supabase, conv.company_id as string, id)
|
|
return NextResponse.json({ data: { cleared } })
|
|
},
|
|
)
|