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
90 changes: 90 additions & 0 deletions app/api/routes-b/discounts/[id]/route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { NextRequest } from 'next/server'
import { DELETE } from './route'

vi.mock('@/lib/db', () => ({
prisma: {
user: { findUnique: vi.fn() },
discount: { findUnique: vi.fn(), delete: vi.fn() },
},
}))
vi.mock('@/lib/auth', () => ({ verifyAuthToken: vi.fn() }))
vi.mock('@/lib/logger', () => ({ logger: { info: vi.fn(), error: vi.fn() } }))

import { prisma } from '@/lib/db'
import { verifyAuthToken } from '@/lib/auth'

const mockVerify = verifyAuthToken as unknown as ReturnType<typeof vi.fn>
const mockUserFindUnique = prisma.user.findUnique as unknown as ReturnType<typeof vi.fn>
const mockDiscountFindUnique = prisma.discount.findUnique as unknown as ReturnType<typeof vi.fn>
const mockDiscountDelete = prisma.discount.delete as unknown as ReturnType<typeof vi.fn>

function makeDelete(id: string, token: string | null = 'Bearer valid-token') {
const headers: Record<string, string> = {}
if (token) headers.authorization = token
return new NextRequest(`http://localhost/api/routes-b/discounts/${id}`, {
method: 'DELETE',
headers,
})
}

function callDelete(id: string, token: string | null = 'Bearer valid-token') {
return DELETE(makeDelete(id, token), { params: Promise.resolve({ id }) })
}

const mockDiscount = { id: 'disc-1', userId: 'user-1' }

beforeEach(() => {
vi.clearAllMocks()
mockVerify.mockResolvedValue({ userId: 'privy-1' })
mockUserFindUnique.mockResolvedValue({ id: 'user-1' })
mockDiscountFindUnique.mockResolvedValue(mockDiscount)
mockDiscountDelete.mockResolvedValue(mockDiscount)
})

describe('DELETE /api/routes-b/discounts/[id]', () => {
it('returns 401 when unauthenticated', async () => {
const res = await callDelete('disc-1', null)
expect(res.status).toBe(401)
})

it('returns 401 when the token is invalid', async () => {
mockVerify.mockResolvedValue(null)
const res = await callDelete('disc-1')
expect(res.status).toBe(401)
})

it('returns 404 when the user record is missing', async () => {
mockUserFindUnique.mockResolvedValue(null)
const res = await callDelete('disc-1')
expect(res.status).toBe(404)
})

it('returns 404 when the discount does not exist', async () => {
mockDiscountFindUnique.mockResolvedValue(null)
const res = await callDelete('missing')
expect(res.status).toBe(404)
expect(mockDiscountDelete).not.toHaveBeenCalled()
})

it('returns 403 when the discount belongs to another user', async () => {
mockDiscountFindUnique.mockResolvedValue({ ...mockDiscount, userId: 'someone-else' })
const res = await callDelete('disc-1')
expect(res.status).toBe(403)
expect(mockDiscountDelete).not.toHaveBeenCalled()
})

it('deletes the discount and returns 200 on the happy path', async () => {
const res = await callDelete('disc-1')
expect(res.status).toBe(200)
const json = await res.json()
expect(json.id).toBe('disc-1')
expect(mockDiscountDelete).toHaveBeenCalledWith({ where: { id: 'disc-1' } })
})

it('returns 500 when an unexpected error occurs', async () => {
mockDiscountDelete.mockRejectedValue(new Error('db unavailable'))
const res = await callDelete('disc-1')
expect(res.status).toBe(500)
})
})
43 changes: 43 additions & 0 deletions app/api/routes-b/discounts/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/db'
import { verifyAuthToken } from '@/lib/auth'
import { logger } from '@/lib/logger'

// DELETE /api/routes-b/discounts/[id] — delete a discount code owned by the
// authenticated user.

export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
try {
const { id } = await params
const authToken = request.headers.get('authorization')?.replace('Bearer ', '')
if (!authToken) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

const claims = await verifyAuthToken(authToken)
if (!claims) return NextResponse.json({ error: 'Invalid token' }, { status: 401 })

const user = await prisma.user.findUnique({ where: { privyId: claims.userId } })
if (!user) return NextResponse.json({ error: 'User not found' }, { status: 404 })

const discount = await prisma.discount.findUnique({
where: { id },
select: { id: true, userId: true },
})

if (!discount) {
return NextResponse.json({ error: 'Discount not found' }, { status: 404 })
}
if (discount.userId !== user.id) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}

await prisma.discount.delete({ where: { id } })

return NextResponse.json({ id }, { status: 200 })
} catch (error) {
logger.error({ err: error }, 'DELETE /api/routes-b/discounts/[id] error')
return NextResponse.json({ error: 'Failed to delete discount' }, { status: 500 })
}
}
94 changes: 94 additions & 0 deletions app/api/routes-b/invoices/templates/[id]/route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { NextRequest } from 'next/server'
import { GET } from './route'

vi.mock('@/lib/db', () => ({
prisma: {
user: { findUnique: vi.fn() },
invoiceTemplate: { findUnique: vi.fn() },
},
}))
vi.mock('@/lib/auth', () => ({ verifyAuthToken: vi.fn() }))
vi.mock('@/lib/logger', () => ({ logger: { info: vi.fn(), error: vi.fn() } }))

import { prisma } from '@/lib/db'
import { verifyAuthToken } from '@/lib/auth'

const mockVerify = verifyAuthToken as unknown as ReturnType<typeof vi.fn>
const mockUserFindUnique = prisma.user.findUnique as unknown as ReturnType<typeof vi.fn>
const mockTemplateFindUnique = prisma.invoiceTemplate.findUnique as unknown as ReturnType<typeof vi.fn>

function makeGet(id: string, token: string | null = 'Bearer valid-token') {
const headers: Record<string, string> = {}
if (token) headers.authorization = token
return new NextRequest(`http://localhost/api/routes-b/invoices/templates/${id}`, { headers })
}

function callGet(id: string, token: string | null = 'Bearer valid-token') {
return GET(makeGet(id, token), { params: Promise.resolve({ id }) })
}

const mockTemplate = {
id: 'tmpl-1',
userId: 'user-1',
name: 'Standard Web Project',
clientEmail: 'client@example.com',
clientName: 'Acme Corp',
description: 'Website redesign',
amount: { toString: () => '1500' },
currency: 'USD',
createdAt: new Date('2026-08-01'),
updatedAt: new Date('2026-08-01'),
}

beforeEach(() => {
vi.clearAllMocks()
mockVerify.mockResolvedValue({ userId: 'privy-1' })
mockUserFindUnique.mockResolvedValue({ id: 'user-1' })
mockTemplateFindUnique.mockResolvedValue(mockTemplate)
})

describe('GET /api/routes-b/invoices/templates/[id]', () => {
it('returns 401 when unauthenticated', async () => {
const res = await callGet('tmpl-1', null)
expect(res.status).toBe(401)
})

it('returns 401 when the token is invalid', async () => {
mockVerify.mockResolvedValue(null)
const res = await callGet('tmpl-1')
expect(res.status).toBe(401)
})

it('returns 404 when the user record is missing', async () => {
mockUserFindUnique.mockResolvedValue(null)
const res = await callGet('tmpl-1')
expect(res.status).toBe(404)
})

it('returns 404 when the template does not exist', async () => {
mockTemplateFindUnique.mockResolvedValue(null)
const res = await callGet('missing')
expect(res.status).toBe(404)
})

it('returns 403 when the template belongs to another user', async () => {
mockTemplateFindUnique.mockResolvedValue({ ...mockTemplate, userId: 'someone-else' })
const res = await callGet('tmpl-1')
expect(res.status).toBe(403)
})

it('returns 200 with the template on the happy path', async () => {
const res = await callGet('tmpl-1')
expect(res.status).toBe(200)
const json = await res.json()
expect(json.template.id).toBe('tmpl-1')
expect(json.template.amount).toBe(1500)
})

it('returns 500 when an unexpected error occurs', async () => {
mockTemplateFindUnique.mockRejectedValue(new Error('db unavailable'))
const res = await callGet('tmpl-1')
expect(res.status).toBe(500)
})
})
64 changes: 64 additions & 0 deletions app/api/routes-b/invoices/templates/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/db'
import { verifyAuthToken } from '@/lib/auth'
import { logger } from '@/lib/logger'

// GET /api/routes-b/invoices/templates/[id] — fetch a single invoice
// template owned by the authenticated user.

export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
try {
const { id } = await params
const authToken = request.headers.get('authorization')?.replace('Bearer ', '')
if (!authToken) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

const claims = await verifyAuthToken(authToken)
if (!claims) return NextResponse.json({ error: 'Invalid token' }, { status: 401 })

const user = await prisma.user.findUnique({ where: { privyId: claims.userId } })
if (!user) return NextResponse.json({ error: 'User not found' }, { status: 404 })

const template = await prisma.invoiceTemplate.findUnique({
where: { id },
select: {
id: true,
userId: true,
name: true,
clientEmail: true,
clientName: true,
description: true,
amount: true,
currency: true,
createdAt: true,
updatedAt: true,
},
})

if (!template) {
return NextResponse.json({ error: 'Invoice template not found' }, { status: 404 })
}
if (template.userId !== user.id) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}

return NextResponse.json({
template: {
id: template.id,
name: template.name,
clientEmail: template.clientEmail,
clientName: template.clientName,
description: template.description,
amount: Number(template.amount),
currency: template.currency,
createdAt: template.createdAt.toISOString(),
updatedAt: template.updatedAt.toISOString(),
},
})
} catch (error) {
logger.error({ err: error }, 'GET /api/routes-b/invoices/templates/[id] error')
return NextResponse.json({ error: 'Failed to fetch invoice template' }, { status: 500 })
}
}
Loading
Loading