Skip to content

Commit 9c95c57

Browse files
authored
Merge pull request davedumto#460 from ACOB-DEV/codex/routes-b-359-360-361-399
feat: add routes-b wallet, invoice, and transaction handlers
2 parents d01f13d + 6c51511 commit 9c95c57

4 files changed

Lines changed: 270 additions & 20 deletions

File tree

app/api/routes-b/invoices/[id]/route.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,11 @@ export async function GET(
2424
return NextResponse.json({ error: 'User not found' }, { status: 404 })
2525
}
2626

27-
const invoice = await prisma.invoice.findFirst({
28-
where: { id, userId: user.id },
27+
const invoice = await prisma.invoice.findUnique({
28+
where: { id },
2929
select: {
3030
id: true,
31+
userId: true,
3132
invoiceNumber: true,
3233
clientEmail: true,
3334
clientName: true,
@@ -47,7 +48,26 @@ export async function GET(
4748
return NextResponse.json({ error: 'Invoice not found' }, { status: 404 })
4849
}
4950

50-
return NextResponse.json({ ...invoice, amount: Number(invoice.amount) })
51+
if (invoice.userId !== user.id) {
52+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
53+
}
54+
55+
return NextResponse.json({
56+
invoice: {
57+
id: invoice.id,
58+
invoiceNumber: invoice.invoiceNumber,
59+
clientName: invoice.clientName,
60+
clientEmail: invoice.clientEmail,
61+
description: invoice.description,
62+
amount: Number(invoice.amount),
63+
currency: invoice.currency,
64+
status: invoice.status,
65+
paymentLink: invoice.paymentLink,
66+
dueDate: invoice.dueDate,
67+
paidAt: invoice.paidAt,
68+
createdAt: invoice.createdAt,
69+
},
70+
})
5171
}
5272

5373
export async function PATCH(

app/api/routes-b/invoices/route.ts

Lines changed: 108 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,62 @@
11
import { NextRequest, NextResponse } from 'next/server'
22
import { prisma } from '@/lib/db'
33
import { 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+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { NextRequest, NextResponse } from 'next/server'
2+
import { prisma } from '@/lib/db'
3+
import { verifyAuthToken } from '@/lib/auth'
4+
5+
const ALLOWED_TYPES = new Set(['payment', 'withdrawal'])
6+
7+
function parseDateParam(value: string | null, fieldName: 'from' | 'to') {
8+
if (!value) {
9+
return null
10+
}
11+
12+
const date = new Date(value)
13+
if (Number.isNaN(date.getTime())) {
14+
return { error: `${fieldName} must be a valid ISO date string` }
15+
}
16+
17+
return { date }
18+
}
19+
20+
export async function GET(request: NextRequest) {
21+
const authToken = request.headers.get('authorization')?.replace('Bearer ', '')
22+
const claims = await verifyAuthToken(authToken || '')
23+
if (!claims) {
24+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
25+
}
26+
27+
const user = await prisma.user.findUnique({ where: { privyId: claims.userId } })
28+
if (!user) {
29+
return NextResponse.json({ error: 'User not found' }, { status: 404 })
30+
}
31+
32+
const url = new URL(request.url)
33+
const type = url.searchParams.get('type')
34+
const from = parseDateParam(url.searchParams.get('from'), 'from')
35+
const to = parseDateParam(url.searchParams.get('to'), 'to')
36+
const page = Math.max(1, Number.parseInt(url.searchParams.get('page') || '1', 10) || 1)
37+
const limit = Math.min(
38+
100,
39+
Math.max(1, Number.parseInt(url.searchParams.get('limit') || '20', 10) || 20),
40+
)
41+
42+
if (type && !ALLOWED_TYPES.has(type)) {
43+
return NextResponse.json(
44+
{ error: 'Invalid type. Allowed values are payment or withdrawal' },
45+
{ status: 400 },
46+
)
47+
}
48+
49+
if (from && 'error' in from) {
50+
return NextResponse.json({ error: from.error }, { status: 400 })
51+
}
52+
53+
if (to && 'error' in to) {
54+
return NextResponse.json({ error: to.error }, { status: 400 })
55+
}
56+
57+
const createdAt =
58+
from?.date || to?.date
59+
? {
60+
...(from?.date ? { gte: from.date } : {}),
61+
...(to?.date ? { lte: to.date } : {}),
62+
}
63+
: undefined
64+
65+
const where = {
66+
userId: user.id,
67+
...(type ? { type } : {}),
68+
...(createdAt ? { createdAt } : {}),
69+
}
70+
71+
const [transactions, total] = await Promise.all([
72+
prisma.transaction.findMany({
73+
where,
74+
orderBy: { createdAt: 'desc' },
75+
skip: (page - 1) * limit,
76+
take: limit,
77+
include: {
78+
invoice: {
79+
select: {
80+
invoiceNumber: true,
81+
},
82+
},
83+
},
84+
}),
85+
prisma.transaction.count({ where }),
86+
])
87+
88+
return NextResponse.json({
89+
transactions: transactions.map((transaction) => ({
90+
id: transaction.id,
91+
type: transaction.type,
92+
status: transaction.status,
93+
amount: Number(transaction.amount),
94+
currency: transaction.currency,
95+
description: transaction.invoice?.invoiceNumber
96+
? `Invoice ${transaction.invoice.invoiceNumber} paid`
97+
: transaction.type === 'withdrawal'
98+
? 'Withdrawal initiated'
99+
: 'Transaction recorded',
100+
createdAt: transaction.createdAt,
101+
})),
102+
pagination: {
103+
page,
104+
limit,
105+
total,
106+
totalPages: Math.ceil(total / limit),
107+
},
108+
})
109+
}

app/api/routes-b/wallet/route.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { NextRequest, NextResponse } from 'next/server'
2+
import { prisma } from '@/lib/db'
3+
import { verifyAuthToken } from '@/lib/auth'
4+
5+
export async function GET(request: NextRequest) {
6+
const authToken = request.headers.get('authorization')?.replace('Bearer ', '')
7+
const claims = await verifyAuthToken(authToken || '')
8+
9+
if (!claims) {
10+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
11+
}
12+
13+
const user = await prisma.user.findUnique({ where: { privyId: claims.userId } })
14+
if (!user) {
15+
return NextResponse.json({ error: 'User not found' }, { status: 404 })
16+
}
17+
18+
const wallet = await prisma.wallet.findUnique({ where: { userId: user.id } })
19+
if (!wallet) {
20+
return NextResponse.json({ wallet: null }, { status: 200 })
21+
}
22+
23+
return NextResponse.json({
24+
wallet: {
25+
id: wallet.id,
26+
stellarAddress: wallet.address,
27+
createdAt: wallet.createdAt,
28+
},
29+
})
30+
}

0 commit comments

Comments
 (0)