Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 22 additions & 7 deletions app/api/milestones/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,35 @@ export const dynamic = 'force-dynamic'
import { NextRequest, NextResponse } from 'next/server'
import { withAuth } from '@/lib/auth/middleware'
import { sql } from '@/lib/db'
import { UpdateMilestoneSchema, IMMUTABLE_MILESTONE_STATUS_VALUES } from '@/lib/validations'

// Only the project client can update a milestone (and only when not yet submitted/approved/paid)
export const PATCH = withAuth(async (request: NextRequest, auth) => {
const id = request.nextUrl.pathname.split('/').at(-1)

let body: unknown
try {
const body = await request.json()
const { title, description, amount, currency, due_date, sort_order, deliverables } = body
body = await request.json()
} catch {
return NextResponse.json(
{ error: 'Request body must be valid JSON', code: 'INVALID_JSON' },
{ status: 400 }
)
}

const parsed = UpdateMilestoneSchema.safeParse(body)
if (!parsed.success) {
return NextResponse.json(
{ error: 'Validation failed', code: 'INVALID_REQUEST_BODY', details: parsed.error.flatten().fieldErrors },
{ status: 422 }
)
}

const { title, description, amount, currency, due_date, sort_order, deliverables } = parsed.data

try {
const [user] = await sql`SELECT id FROM users WHERE wallet_address = ${auth.walletAddress} LIMIT 1`
if (!user) return NextResponse.json({ error: 'User not found', code: 'USER_NOT_FOUND' }, { status: 404 })

// Fetch milestone and verify ownership via project
const [milestone] = await sql`
SELECT m.*, p.client_id
FROM milestones m
Expand All @@ -28,8 +44,7 @@ export const PATCH = withAuth(async (request: NextRequest, auth) => {
return NextResponse.json({ error: 'Access denied', code: 'FORBIDDEN' }, { status: 403 })
}

const immutableStatuses = ['submitted', 'approved', 'paid']
if (immutableStatuses.includes(milestone.status)) {
if ((IMMUTABLE_MILESTONE_STATUS_VALUES as readonly string[]).includes(milestone.status)) {
return NextResponse.json(
{ error: `Cannot update a milestone with status '${milestone.status}'`, code: 'INVALID_STATUS' },
{ status: 422 }
Expand All @@ -42,7 +57,7 @@ export const PATCH = withAuth(async (request: NextRequest, auth) => {
description = COALESCE(${description ?? null}, description),
amount = COALESCE(${amount ?? null}, amount),
currency = COALESCE(${currency ?? null}, currency),
due_date = COALESCE(${due_date ?? null}, due_date),
due_date = COALESCE(${due_date ? due_date.toISOString() : null}, due_date),
sort_order = COALESCE(${sort_order ?? null}, sort_order),
deliverables = COALESCE(${deliverables ? JSON.stringify(deliverables) : null}, deliverables),
updated_at = NOW()
Expand Down
36 changes: 23 additions & 13 deletions app/api/milestones/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,30 @@ export const dynamic = 'force-dynamic'
import { NextRequest, NextResponse } from 'next/server'
import { withAuth } from '@/lib/auth/middleware'
import { sql } from '@/lib/db'
import { CreateMilestoneSchema } from '@/lib/validations'

export const POST = withAuth(async (request: NextRequest, auth) => {
let body: unknown
try {
const body = await request.json()
const { project_id, title, description, amount, currency, due_date, sort_order, deliverables } = body
body = await request.json()
} catch {
return NextResponse.json(
{ error: 'Request body must be valid JSON', code: 'INVALID_JSON' },
{ status: 400 }
)
}

if (!project_id || !title || amount === undefined) {
return NextResponse.json(
{ error: 'Missing required fields: project_id, title, amount', code: 'MISSING_FIELDS' },
{ status: 400 }
)
}
const parsed = CreateMilestoneSchema.safeParse(body)
if (!parsed.success) {
return NextResponse.json(
{ error: 'Validation failed', code: 'INVALID_REQUEST_BODY', details: parsed.error.flatten().fieldErrors },
{ status: 422 }
)
}

const { project_id, title, description, amount, currency, due_date, sort_order, deliverables } = parsed.data

// Verify caller is the project owner (client)
try {
const [user] = await sql`SELECT id FROM users WHERE wallet_address = ${auth.walletAddress} LIMIT 1`
if (!user) return NextResponse.json({ error: 'User not found', code: 'USER_NOT_FOUND' }, { status: 404 })

Expand All @@ -32,10 +42,10 @@ export const POST = withAuth(async (request: NextRequest, auth) => {
${title},
${description ?? null},
${amount},
${currency ?? 'USDC'},
${due_date ?? null},
${sort_order ?? 0},
${JSON.stringify(deliverables ?? [])}
${currency},
${due_date ? due_date.toISOString() : null},
${sort_order},
${JSON.stringify(deliverables)}
)
RETURNING *
`
Expand Down
Loading
Loading