11import { NextRequest , NextResponse } from 'next/server'
22import { prisma } from '@/lib/db'
33import { verifyAuthToken } from '@/lib/auth'
4+ import { generateInvoiceNumber } from '@/lib/utils'
45
5- export async function GET ( request : NextRequest ) {
6+ async function getAuthenticatedUser ( request : NextRequest ) {
67 const authToken = request . headers . get ( 'authorization' ) ?. replace ( 'Bearer ' , '' )
78 const claims = await verifyAuthToken ( authToken || '' )
8- if ( ! claims ) return NextResponse . json ( { error : 'Unauthorized' } , { status : 401 } )
9+ if ( ! claims ) {
10+ return { error : NextResponse . json ( { error : 'Unauthorized' } , { status : 401 } ) }
11+ }
912
1013 const user = await prisma . user . findUnique ( { where : { privyId : claims . userId } } )
11- if ( ! user ) return NextResponse . json ( { error : 'User not found' } , { status : 404 } )
14+ if ( ! user ) {
15+ return { error : NextResponse . json ( { error : 'User not found' } , { status : 404 } ) }
16+ }
17+
18+ return { user }
19+ }
20+
21+ async function getUniqueInvoiceNumber ( ) {
22+ for ( let attempt = 0 ; attempt < 5 ; attempt += 1 ) {
23+ const invoiceNumber = generateInvoiceNumber ( )
24+ const existingInvoice = await prisma . invoice . findUnique ( {
25+ where : { invoiceNumber } ,
26+ select : { id : true } ,
27+ } )
28+
29+ if ( ! existingInvoice ) {
30+ return invoiceNumber
31+ }
32+ }
33+
34+ throw new Error ( 'Failed to generate a unique invoice number' )
35+ }
36+
37+ export async function GET ( request : NextRequest ) {
38+ const auth = await getAuthenticatedUser ( request )
39+ if ( 'error' in auth ) {
40+ return auth . error
41+ }
1242
1343 const { searchParams } = new URL ( request . url )
1444 const status = searchParams . get ( 'status' )
15- const page = parseInt ( searchParams . get ( 'page' ) || '1' )
16- const limit = Math . min ( parseInt ( searchParams . get ( 'limit' ) || '20' ) , 50 )
45+ const page = Math . max ( 1 , Number . parseInt ( searchParams . get ( 'page' ) || '1' , 10 ) || 1 )
46+ const limit = Math . min (
47+ 50 ,
48+ Math . max ( 1 , Number . parseInt ( searchParams . get ( 'limit' ) || '20' , 10 ) || 20 ) ,
49+ )
1750
1851 const validStatuses = [ 'pending' , 'paid' , 'overdue' , 'cancelled' ]
1952 if ( status && ! validStatuses . includes ( status ) ) {
2053 return NextResponse . json ( { error : 'Invalid status' } , { status : 400 } )
2154 }
2255
23- const where : any = { userId : user . id }
24- if ( status ) where . status = status
56+ const where = {
57+ userId : auth . user . id ,
58+ ...( status ? { status } : { } ) ,
59+ }
2560
2661 const total = await prisma . invoice . count ( { where } )
2762 const invoices = await prisma . invoice . findMany ( {
@@ -39,23 +74,79 @@ export async function GET(request: NextRequest) {
3974 status : true ,
4075 dueDate : true ,
4176 createdAt : true ,
42- }
77+ } ,
4378 } )
4479
45- const totalPages = Math . ceil ( total / limit )
46-
47- const response = {
48- invoices : invoices . map ( inv => ( {
49- ...inv ,
50- amount : parseFloat ( inv . amount . toString ( ) )
80+ return NextResponse . json ( {
81+ invoices : invoices . map ( ( invoice ) => ( {
82+ ...invoice ,
83+ amount : Number ( invoice . amount ) ,
5184 } ) ) ,
5285 pagination : {
5386 page,
5487 limit,
5588 total,
56- totalPages
89+ totalPages : Math . ceil ( total / limit ) ,
90+ } ,
91+ } )
92+ }
93+
94+ export async function POST ( request : NextRequest ) {
95+ const auth = await getAuthenticatedUser ( request )
96+ if ( 'error' in auth ) {
97+ return auth . error
98+ }
99+
100+ const body = await request . json ( )
101+ const { clientEmail, clientName, description, amount, currency = 'USD' , dueDate } = body
102+
103+ if ( ! clientEmail || ! description || amount === undefined || amount === null ) {
104+ return NextResponse . json (
105+ { error : 'clientEmail, description, and amount are required' } ,
106+ { status : 400 } ,
107+ )
108+ }
109+
110+ const parsedAmount = Number ( amount )
111+ if ( ! Number . isFinite ( parsedAmount ) || parsedAmount <= 0 ) {
112+ return NextResponse . json ( { error : 'amount must be greater than 0' } , { status : 400 } )
113+ }
114+
115+ let parsedDueDate : Date | null = null
116+ if ( dueDate ) {
117+ parsedDueDate = new Date ( dueDate )
118+ if ( Number . isNaN ( parsedDueDate . getTime ( ) ) ) {
119+ return NextResponse . json ( { error : 'dueDate must be a valid date string' } , { status : 400 } )
57120 }
58121 }
59122
60- return NextResponse . json ( response )
61- }
123+ const invoiceNumber = await getUniqueInvoiceNumber ( )
124+ const baseUrl = process . env . NEXT_PUBLIC_APP_URL || `https://${ request . headers . get ( 'host' ) } `
125+ const paymentLink = `${ baseUrl } /pay/${ invoiceNumber } `
126+
127+ const invoice = await prisma . invoice . create ( {
128+ data : {
129+ userId : auth . user . id ,
130+ invoiceNumber,
131+ clientEmail : String ( clientEmail ) . toLowerCase ( ) ,
132+ clientName : clientName || null ,
133+ description,
134+ amount : parsedAmount ,
135+ currency,
136+ paymentLink,
137+ dueDate : parsedDueDate ,
138+ } ,
139+ } )
140+
141+ return NextResponse . json (
142+ {
143+ id : invoice . id ,
144+ invoiceNumber : invoice . invoiceNumber ,
145+ paymentLink : invoice . paymentLink ,
146+ status : invoice . status ,
147+ amount : Number ( invoice . amount ) ,
148+ currency : invoice . currency ,
149+ } ,
150+ { status : 201 } ,
151+ )
152+ }
0 commit comments