|
| 1 | +import { NextRequest, NextResponse } from "next/server"; |
| 2 | +import { createServerClient } from "@/lib/supabase"; |
| 3 | + |
| 4 | +interface RouteParams { |
| 5 | + params: Promise<{ token: string; taskId: string }>; |
| 6 | +} |
| 7 | + |
| 8 | +export async function POST(request: NextRequest, { params }: RouteParams) { |
| 9 | + const { token, taskId } = await params; |
| 10 | + const supabase = createServerClient(); |
| 11 | + |
| 12 | + // 1. Validate portal token |
| 13 | + const { data: portalToken, error: tokenError } = await supabase |
| 14 | + .from("portal_tokens") |
| 15 | + .select("*, customers(*)") |
| 16 | + .eq("token", token) |
| 17 | + .single(); |
| 18 | + |
| 19 | + if (tokenError || !portalToken) { |
| 20 | + return NextResponse.json({ error: "Invalid portal token" }, { status: 401 }); |
| 21 | + } |
| 22 | + |
| 23 | + // Check expiry |
| 24 | + if (portalToken.expires_at && new Date(portalToken.expires_at) < new Date()) { |
| 25 | + return NextResponse.json({ error: "Portal token has expired" }, { status: 401 }); |
| 26 | + } |
| 27 | + |
| 28 | + const customer = portalToken.customers as { id: string; name: string; csm_id: string | null }; |
| 29 | + |
| 30 | + // 2. Fetch the task and verify ownership |
| 31 | + const { data: task, error: taskError } = await supabase |
| 32 | + .from("tasks") |
| 33 | + .select("*") |
| 34 | + .eq("id", taskId) |
| 35 | + .eq("customer_id", customer.id) |
| 36 | + .single(); |
| 37 | + |
| 38 | + if (taskError || !task) { |
| 39 | + return NextResponse.json({ error: "Task not found" }, { status: 404 }); |
| 40 | + } |
| 41 | + |
| 42 | + // 3. Verify task is customer-owned |
| 43 | + if (!task.customer_owned) { |
| 44 | + return NextResponse.json( |
| 45 | + { error: "This task cannot be completed by the customer" }, |
| 46 | + { status: 403 } |
| 47 | + ); |
| 48 | + } |
| 49 | + |
| 50 | + // 4. Idempotent: already complete → return 200 without notifying |
| 51 | + if (task.completed) { |
| 52 | + return NextResponse.json({ success: true, already_complete: true }); |
| 53 | + } |
| 54 | + |
| 55 | + // 5. Mark task complete |
| 56 | + const now = new Date().toISOString(); |
| 57 | + const { error: updateError } = await supabase |
| 58 | + .from("tasks") |
| 59 | + .update({ |
| 60 | + completed: true, |
| 61 | + completed_by: "customer", |
| 62 | + completed_at: now, |
| 63 | + updated_at: now, |
| 64 | + }) |
| 65 | + .eq("id", taskId); |
| 66 | + |
| 67 | + if (updateError) { |
| 68 | + console.error("Failed to update task:", updateError); |
| 69 | + return NextResponse.json({ error: "Failed to complete task" }, { status: 500 }); |
| 70 | + } |
| 71 | + |
| 72 | + // 6. Trigger CSM notification |
| 73 | + if (customer.csm_id) { |
| 74 | + const portalLink = `${process.env.NEXT_PUBLIC_APP_URL}/portal/${token}`; |
| 75 | + const message = `${customer.name} completed '${task.title}'`; |
| 76 | + |
| 77 | + const { error: notifError } = await supabase.from("notifications").insert({ |
| 78 | + recipient_csm_id: customer.csm_id, |
| 79 | + message, |
| 80 | + link: portalLink, |
| 81 | + }); |
| 82 | + |
| 83 | + if (notifError) { |
| 84 | + // Non-fatal: log but don't fail the request |
| 85 | + console.error("Failed to create CSM notification:", notifError); |
| 86 | + } |
| 87 | + } |
| 88 | + |
| 89 | + return NextResponse.json({ success: true }); |
| 90 | +} |
0 commit comments