Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
d59fa2a
feat(api): implement GET /api/routes-b/invoices/[id]/messages
Demilade01 Mar 27, 2026
fdedd63
Module '@prisma/client' has no exported member 'Invoice'.
Demilade01 Mar 27, 2026
40c3e38
feat: add PATCH /api/routes-b/profile to update display name (#366)
okekefrancis112 Mar 27, 2026
879b157
Merge branch 'main' into overdue-invoice
Demilade01 Mar 28, 2026
13a9d21
Merge branch 'main' into main
Demilade01 Mar 28, 2026
58a1277
feat(api): implement GET /api/routes-d/invoices/[id]/preview
Demilade01 Mar 28, 2026
9bef8ee
feat(routes-b): implement invoice PDF, preview, paid list, and audit …
Themancalledpg Mar 28, 2026
7e65bc9
Merge pull request #461 from Themancalledpg/feature/invoice-pdf-download
davedumto Mar 28, 2026
26c5f7a
feat(reminder-settings): add GET endpoint for invoice reminder settings
overprodigy Mar 28, 2026
a0cea7e
feat(contacts): add GET endpoint to list all contacts
overprodigy Mar 28, 2026
633ff79
feat(webhooks): add GET endpoint to list registered webhooks
overprodigy Mar 28, 2026
eb71346
feat(analytics): add GET endpoint for withdrawal stats summary
overprodigy Mar 28, 2026
eb37165
Merge pull request #462 from overprodigy/drips-contributions
davedumto Mar 28, 2026
f0ac465
implement tag list to applied invoice
OnyemaAnthony Mar 28, 2026
12d4256
implement tag creation routes
OnyemaAnthony Mar 28, 2026
ed2f9dd
implement apply a tag to an invoice
OnyemaAnthony Mar 28, 2026
53d4bc4
implement api for tag deletion
OnyemaAnthony Mar 28, 2026
0c6e0a0
feat: implement routes-d endpoints #331 #332 #335 #340
Fran19-09 Mar 29, 2026
7f0c039
Merge pull request #468 from Fran19-09/feature/wave-issues-fran
davedumto Mar 29, 2026
70413b0
feat: add DELETE /api/routes-b/invoices/[id]/tags/[tagId] handler
KevinMB0220 Mar 29, 2026
552c4de
fix: correct prisma import path in offramp webhook route
KevinMB0220 Mar 29, 2026
f493fbb
fix: await params as Promise in routes-b dynamic handlers
KevinMB0220 Mar 29, 2026
915e156
fix: resolve remaining build errors in routes-b pdf and offramp webhook
KevinMB0220 Mar 29, 2026
235675f
Merge pull request #469 from KevinMB0220/feat/routes-b-delete-invoice…
davedumto Mar 29, 2026
fe93728
Merge pull request #395 from Demilade01/main
davedumto Mar 29, 2026
5d2b850
Merge pull request #463 from Demilade01/invoice-preview
davedumto Mar 29, 2026
7cda9be
feat: add routes-d dashboard and bank account APIs
Akpamgbo Mar 29, 2026
70ec073
feat(#487): Add GET handler for listing tags on an invoice
OsagieCynthia Mar 29, 2026
64477ff
feat(#476): Add GET handler for retrieving a single withdrawal by ID
OsagieCynthia Mar 29, 2026
de70370
feat(#486): Add DELETE handler for removing a tag
OsagieCynthia Mar 29, 2026
7bdcc82
feat(#474): Add GET handler for listing cancelled invoices with pagin…
OsagieCynthia Mar 29, 2026
40304f4
Merge pull request #492 from OsagieCynthia/feat/routes-b-invoice-with…
davedumto Mar 29, 2026
72231b7
feat: add client GET/PATCH, amount PATCH, and stats GET routes
Hahfyeex Mar 29, 2026
361f845
Merge pull request #493 from Hahfyeex/feat/routes-b-client-amount-stats
davedumto Mar 29, 2026
a52767c
feat: add pending invoices endpoint and simplify tag response mapping
jahrulezfrancis Mar 29, 2026
6997efd
refactor: simplify tag mapping and remove createdAt field from tags A…
jahrulezfrancis Mar 29, 2026
b4d9bcc
Merge pull request #491 from Akpamgbo/codex/issues-309-310-312-319-ro…
davedumto Mar 29, 2026
f66c314
Merge pull request #494 from jahrulezfrancis/feature/list-pending-inv…
davedumto Mar 29, 2026
8af8c4d
feat: implement PATCH handler for tags (#490)
jahrulezfrancis Mar 29, 2026
efa801b
Merge pull request #495 from jahrulezfrancis/feat/routes-b-update-tag
davedumto Mar 29, 2026
2bd5cf9
feat(api): POST routes-d invoice payment reminder (#323)
Nwanne-san Mar 29, 2026
f7f17c2
feat(api): GET routes-d six-month invoice summary (#345)
Nwanne-san Mar 29, 2026
795fd87
feat(api): GET routes-b invoice messages thread (#422)
Nwanne-san Mar 29, 2026
f3d75a6
docs(api): document routes-b bank-accounts GET (#362)
Nwanne-san Mar 29, 2026
ba4ce6d
backend implementations
Wilfred007 Mar 30, 2026
65c108a
Merge pull request #502 from Open-Works-Contributions/backend-impleme…
davedumto Mar 30, 2026
83e8853
Merge pull request #500 from Nwanne-san/feat/362-routes-b-bank-accounts
davedumto Mar 30, 2026
36789b7
Merge pull request #499 from Nwanne-san/feat/422-routes-b-invoice-mes…
davedumto Mar 30, 2026
c342c86
Merge pull request #498 from Nwanne-san/feat/345-routes-d-invoice-sum…
davedumto Mar 30, 2026
d8c8af8
Merge pull request #497 from Nwanne-san/feat/323-routes-d-invoice-remind
davedumto Mar 30, 2026
5a66aec
issues solved
Peolite001 Mar 30, 2026
dbf0ba2
feat(api): add patch handlers for account and invoice routes
FrostGraphix Mar 30, 2026
05ac3a5
Merge branch 'main' into delete-tag
OnyemaAnthony Mar 30, 2026
ed6a491
Merge branch 'main' into create-tag-list
OnyemaAnthony Mar 30, 2026
7e61ecc
Merge branch 'main' into tag-list
OnyemaAnthony Mar 30, 2026
0264b4a
fix: add HMAC-SHA256 signature verification to MoonPay webhook
ebubechi-ihediwa Mar 30, 2026
974260b
test: add verification script for MoonPay webhook signature
ebubechi-ihediwa Mar 30, 2026
0b0ec9c
Merge pull request #467 from OnyemaAnthony/delete-tag
davedumto Mar 30, 2026
30f676d
Merge branch 'main' into apply-tag
davedumto Mar 30, 2026
0c86b69
Merge pull request #466 from OnyemaAnthony/apply-tag
davedumto Mar 30, 2026
498102e
Merge pull request #465 from OnyemaAnthony/create-tag-list
davedumto Mar 30, 2026
47e9075
Merge branch 'main' into tag-list
OnyemaAnthony Mar 30, 2026
9c54790
Merge pull request #464 from OnyemaAnthony/tag-list
davedumto Mar 30, 2026
f4bbb53
Merge pull request #507 from ebubechi-ihediwa/fix-moonpay-webhook-sig…
davedumto Mar 30, 2026
ae635fe
feat: add routes-b invoice duplication endpoint
floxxih Mar 31, 2026
3a1cc48
chore: sync lockfile with declared dependencies
floxxih Mar 31, 2026
3fac271
Merge pull request #508 from floxxih/feat/routes-b-duplicate-invoice-371
davedumto Mar 31, 2026
b6fc641
Merge pull request #506 from FrostGraphix/codex/routes-311-449-452-471
davedumto Apr 1, 2026
a6020e3
Merge branch 'main' into display_name
okekefrancis112 Apr 1, 2026
7617e4d
feat: add routes-b detail GET handlers
Jay-Peter-Egemasi Apr 1, 2026
84d6080
Merge pull request #429 from okekefrancis112/display_name
davedumto Apr 1, 2026
95f08f8
Merge branch 'main' into main
davedumto Apr 1, 2026
cfff946
Merge pull request #505 from Peolite001/main
davedumto Apr 1, 2026
bc76e8b
Merge pull request #501 from Jay-Peter-Egemasi/codex/routes-b-get-end…
davedumto Apr 3, 2026
702c874
feat: implement routes-b stale exchange fallback, authz helper, PAT e…
Apr 28, 2026
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
50 changes: 50 additions & 0 deletions app/api/routes-b/_lib/authz.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { NextRequest } from 'next/server'
import crypto from 'crypto'
import { prisma } from '@/lib/db'
import { verifyAuthToken } from '@/lib/auth'

export class RoutesBForbiddenError extends Error {
code = 'FORBIDDEN'
status = 403
}

type AuthContext = { userId: string; role: string; scopes: string[] }

export async function resolveRoutesBAuth(req: NextRequest): Promise<AuthContext | null> {
const authHeader = req.headers.get('authorization')
const token = authHeader?.startsWith('Bearer ') ? authHeader.slice(7).trim() : ''
if (!token) return null

const claims = await verifyAuthToken(token)
if (claims?.userId) {
const user = await prisma.user.findUnique({ where: { privyId: claims.userId }, select: { id: true, role: true } })
if (!user) return null
return { userId: user.id, role: user.role, scopes: ['routes-b:read'] }
}

const hashedKey = crypto.createHash('sha256').update(token).digest('hex')
const apiKey = await prisma.apiKey.findUnique({ where: { hashedKey }, select: { id: true, userId: true, isActive: true, name: true } })
if (!apiKey || !apiKey.isActive || !apiKey.name.startsWith('routes-b-pat:')) return null

const user = await prisma.user.findUnique({ where: { id: apiKey.userId }, select: { role: true } })
if (!user) return null

await prisma.apiKey.update({ where: { id: apiKey.id }, data: { lastUsedAt: new Date() } })
return { userId: apiKey.userId, role: user.role, scopes: ['routes-b:read'] }
}

export async function requireScope(req: NextRequest, scope: string): Promise<AuthContext> {
const auth = await resolveRoutesBAuth(req)
if (!auth || !auth.scopes.includes(scope)) throw new RoutesBForbiddenError('Missing required scope')
return auth
}

export async function requireRole(req: NextRequest, role: string): Promise<AuthContext> {
const auth = await resolveRoutesBAuth(req)
if (!auth || auth.role !== role) throw new RoutesBForbiddenError('Missing required role')
return auth
}

export function hasScope(scopes: string[], scope: string): boolean {
return scopes.includes(scope)
}
50 changes: 50 additions & 0 deletions app/api/routes-b/analytics/top-months/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/db'
import { verifyAuthToken } from '@/lib/auth'

/**
* GET /api/routes-b/analytics/top-months
* Returns the three calendar months with the highest paid invoice totals for the authenticated user.
*/
export async function GET(request: NextRequest) {
try {
const authToken = request.headers.get('authorization')?.replace('Bearer ', '')
const claims = await verifyAuthToken(authToken || '')
if (!claims) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

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

// Fetch all paid invoices for the user
const paid = await prisma.invoice.findMany({
where: { userId: user.id, status: 'paid' },
select: { amount: true, paidAt: true },
})

// Group by "YYYY-MM" in application code (Prisma does not support month-level groupBy portably)
const monthly: Record<string, number> = {}
for (const inv of paid) {
if (!inv.paidAt) continue
const key = inv.paidAt.toISOString().slice(0, 7) // "2025-01"
monthly[key] = (monthly[key] ?? 0) + Number(inv.amount)
}

// Sort by earned amount descending and take top 3
const topMonths = Object.entries(monthly)
.sort(([, a], [, b]) => b - a)
.slice(0, 3)
.map(([month, earned]) => ({
month,
earned: Number(earned.toFixed(2))
}))

return NextResponse.json({ topMonths })
} catch (error) {
console.error('Top months analytics error:', error)
return NextResponse.json({ error: 'Failed to fetch analytics' }, { status: 500 })
}
}
55 changes: 55 additions & 0 deletions app/api/routes-b/analytics/withdrawals/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/db'
import { verifyAuthToken } from '@/lib/auth'
import { logger } from '@/lib/logger'

export async function GET(request: NextRequest) {
try {
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: 'Unauthorized' }, { status: 401 })
}

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

const where = { userId: user.id, type: 'withdrawal' }

const [total, completed, pending, failed] = await Promise.all([
prisma.transaction.aggregate({
where,
_count: { id: true },
_sum: { amount: true },
}),
prisma.transaction.aggregate({
where: { ...where, status: 'completed' },
_count: { id: true },
_sum: { amount: true },
}),
prisma.transaction.count({ where: { ...where, status: 'pending' } }),
prisma.transaction.count({ where: { ...where, status: 'failed' } }),
])

return NextResponse.json({
withdrawals: {
totalCount: total._count.id,
totalAmount: Number(total._sum.amount ?? 0),
completedCount: completed._count.id,
completedAmount: Number(completed._sum.amount ?? 0),
pendingCount: pending,
failedCount: failed,
currency: 'USDC',
},
})
} catch (error) {
logger.error({ err: error }, 'Routes B analytics withdrawals GET error')
return NextResponse.json({ error: 'Failed to get withdrawal stats' }, { status: 500 })
}
}
43 changes: 43 additions & 0 deletions app/api/routes-b/audit-log/[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'

export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
const { id } = await params

const authToken = request.headers.get('authorization')?.replace('Bearer ', '')
const claims = await verifyAuthToken(authToken || '')
if (!claims) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

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

const event = await prisma.auditEvent.findUnique({ where: { id } })

if (!event) {
return NextResponse.json({ error: 'Audit event not found' }, { status: 404 })
}

if (event.actorId !== user.id) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}

return NextResponse.json({
event: {
id: event.id,
action: event.eventType,
resourceType: 'invoice',
resourceId: event.invoiceId,
ipAddress: null,
userAgent: null,
createdAt: event.createdAt,
},
})
}
5 changes: 3 additions & 2 deletions app/api/routes-b/bank-accounts/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import { verifyAuthToken } from "@/lib/auth";

export async function GET(
request: NextRequest,
{ params }: { params: { id: string } },
{ params }: { params: Promise<{ id: string }> },
) {
const { id } = await params;
const authToken = request.headers
.get("authorization")
?.replace("Bearer ", "");
Expand All @@ -23,7 +24,7 @@ export async function GET(
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });

const bankAccount = await prisma.bankAccount.findUnique({
where: { id: params.id },
where: { id },
});

if (!bankAccount)
Expand Down
1 change: 1 addition & 0 deletions app/api/routes-b/bank-accounts/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ function isValidDigits(value: string, min: number, max: number) {
return pattern.test(value)
}

/** Lists the authenticated user's saved bank accounts (default account first). */
export async function GET(request: NextRequest) {
const authToken = request.headers.get('authorization')?.replace('Bearer ', '')
const claims = await verifyAuthToken(authToken || '')
Expand Down
Loading
Loading