Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
5c7433f
chore: add socket.io dependency for WebSocket support
SweetBoy-eth Jul 11, 2026
5fa6d14
chore: update package-lock.json for socket.io
SweetBoy-eth Jul 11, 2026
d10bb11
feat: update NotificationType enum to match issue requirements
SweetBoy-eth Jul 11, 2026
859927a
feat(#19): add payment validation schemas
SweetBoy-eth Jul 11, 2026
eff7448
feat(#19): add payment processing and transaction recording endpoints
SweetBoy-eth Jul 11, 2026
2217167
feat(#19): add payment routes with webhook support
SweetBoy-eth Jul 11, 2026
5c76cb5
feat(#19): add Stellar transaction verification service
SweetBoy-eth Jul 11, 2026
b06050d
feat(#20): add notification validation schemas
SweetBoy-eth Jul 11, 2026
8775f84
feat(#20): add notification system with CRUD, read status, and bulk o…
SweetBoy-eth Jul 11, 2026
168b4a6
feat(#20): add notification routes with pagination and filtering
SweetBoy-eth Jul 11, 2026
a93d02a
feat(#21): add dashboard analytics endpoints with aggregate queries
SweetBoy-eth Jul 11, 2026
273cafe
feat(#21): add analytics routes with date range filtering
SweetBoy-eth Jul 11, 2026
b482665
feat(#22): add Socket.IO service for real-time WebSocket updates
SweetBoy-eth Jul 11, 2026
08d7fcd
feat: integrate payment, notification, analytics routes and WebSocket…
SweetBoy-eth Jul 11, 2026
48e586d
feat(#22): add WebSocket broadcasts to escrow state changes
SweetBoy-eth Jul 11, 2026
03df892
feat(#22): add WebSocket broadcasts to invoice status changes
SweetBoy-eth Jul 11, 2026
b933681
fix: add BadRequestError to AppError utilities
SweetBoy-eth Jul 11, 2026
e034d46
fix: add AuthRequest interface export to auth middleware
SweetBoy-eth Jul 11, 2026
bda5087
fix: correct import name from getInvoiceById to getInvoice
SweetBoy-eth Jul 11, 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
5,287 changes: 2,645 additions & 2,642 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"multer": "~2.0.1",
"pg": "~8.20.0",
"sanitize-html": "~2.17.6",
"socket.io": "~4.8.3",
"stellar-sdk": "~13.3.0",
"swagger-jsdoc": "~6.3.0",
"swagger-ui-express": "~5.0.1",
Expand Down
8 changes: 4 additions & 4 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ enum PaymentStatus {
}

enum NotificationType {
PAYMENT
ESCROW
INVOICE
SYSTEM
PAYMENT_RECEIVED
ESCROW_FUNDED
INVOICE_SENT
ESCROW_RELEASED
}

model User {
Expand Down
268 changes: 268 additions & 0 deletions src/controllers/analytics.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
import type { Request, Response } from "express";
import { prisma } from "../config/prisma.js";
import type { AuthRequest } from "../middleware/auth.js";

function getDateRange(startDate?: string, endDate?: string): { gte?: Date; lte?: Date } {
const range: { gte?: Date; lte?: Date } = {};
if (startDate) range.gte = new Date(startDate);
if (endDate) range.lte = new Date(endDate);
return range;
}

export async function getDashboardOverview(req: Request, res: Response): Promise<void> {
const authReq = req as AuthRequest;
const userId = authReq.user.userId;

const [totalEarnings, pendingPayments, activeEscrows, totalInvoices] = await Promise.all([
prisma.payment.aggregate({
where: { userId, status: "SUCCESS" },
_sum: { amount: true },
_count: true,
}),
prisma.payment.aggregate({
where: { userId, status: "PENDING" },
_sum: { amount: true },
_count: true,
}),
prisma.escrow.findMany({
where: {
OR: [{ clientId: userId }, { freelancerId: userId }],
status: { in: ["PENDING", "FUNDED"] },
},
select: { id: true, amount: true, status: true, currency: true },
}),
prisma.invoice.count({
where: {
OR: [{ creatorId: userId }, { recipientId: userId }],
},
}),
]);

const pendingEscrowAmount = activeEscrows.reduce(
(sum: number, escrow: { amount: unknown }) => sum + Number(escrow.amount),
0
);

res.json({
success: true,
data: {
totalEarnings: {
amount: totalEarnings._sum.amount ?? 0,
count: totalEarnings._count,
},
pendingPayments: {
amount: pendingPayments._sum.amount ?? 0,
count: pendingPayments._count,
},
escrowSummary: {
active: activeEscrows.length,
pendingAmount: pendingEscrowAmount,
escrows: activeEscrows,
},
totalInvoices,
},
});
}

export async function getMonthlyRevenue(req: Request, res: Response): Promise<void> {
const authReq = req as AuthRequest;
const userId = authReq.user.userId;
const { startDate, endDate } = req.query;

const dateRange = getDateRange(
startDate as string | undefined,
endDate as string | undefined
);

const where: Record<string, unknown> = {
userId,
status: "SUCCESS",
};

if (startDate || endDate) {
where.createdAt = dateRange;
}

const payments = await prisma.payment.findMany({
where,
select: {
amount: true,
currency: true,
createdAt: true,
},
orderBy: { createdAt: "asc" },
});

const monthlyRevenue: Record<string, { usdc: number; count: number }> = {};

for (const payment of payments) {
const monthKey = payment.createdAt.toISOString().slice(0, 7);
if (!monthlyRevenue[monthKey]) {
monthlyRevenue[monthKey] = { usdc: 0, count: 0 };
}
monthlyRevenue[monthKey].usdc += Number(payment.amount);
monthlyRevenue[monthKey].count += 1;
}

const chartData = Object.entries(monthlyRevenue).map(([month, data]) => ({
month,
revenue: data.usdc,
transactions: data.count,
}));

res.json({
success: true,
data: { chartData },
});
}

export async function getTransactionVolume(req: Request, res: Response): Promise<void> {
const authReq = req as AuthRequest;
const userId = authReq.user.userId;
const { startDate, endDate } = req.query;

const dateRange = getDateRange(
startDate as string | undefined,
endDate as string | undefined
);

const where: Record<string, unknown> = {
userId,
};

if (startDate || endDate) {
where.createdAt = dateRange;
}

const [totalPayments, successfulPayments, failedPayments, pendingPayments] = await Promise.all([
prisma.payment.count({ where }),
prisma.payment.count({ where: { ...where, status: "SUCCESS" } }),
prisma.payment.count({ where: { ...where, status: "FAILED" } }),
prisma.payment.count({ where: { ...where, status: "PENDING" } }),
]);

const totalVolume = await prisma.payment.aggregate({
where: { ...where, status: "SUCCESS" },
_sum: { amount: true },
});

res.json({
success: true,
data: {
totalTransactions: totalPayments,
successful: successfulPayments,
failed: failedPayments,
pending: pendingPayments,
totalVolume: totalVolume._sum.amount ?? 0,
},
});
}

export async function getEscrowAnalytics(req: Request, res: Response): Promise<void> {
const authReq = req as AuthRequest;
const userId = authReq.user.userId;
const { startDate, endDate } = req.query;

const dateRange = getDateRange(
startDate as string | undefined,
endDate as string | undefined
);

const where: Record<string, unknown> = {
OR: [{ clientId: userId }, { freelancerId: userId }],
};

if (startDate || endDate) {
where.createdAt = dateRange;
}

const [totalEscrows, pendingEscrows, fundedEscrows, releasedEscrows, refundedEscrows, disputedEscrows] =
await Promise.all([
prisma.escrow.count({ where }),
prisma.escrow.count({ where: { ...where, status: "PENDING" } }),
prisma.escrow.count({ where: { ...where, status: "FUNDED" } }),
prisma.escrow.count({ where: { ...where, status: "RELEASED" } }),
prisma.escrow.count({ where: { ...where, status: "REFUNDED" } }),
prisma.escrow.count({ where: { ...where, status: "DISPUTED" } }),
]);

const totalEscrowAmount = await prisma.escrow.aggregate({
where: { ...where, status: "RELEASED" },
_sum: { amount: true },
});

const pendingEscrowAmount = await prisma.escrow.aggregate({
where: { ...where, status: { in: ["PENDING", "FUNDED"] } },
_sum: { amount: true },
});

res.json({
success: true,
data: {
totalEscrows,
byStatus: {
pending: pendingEscrows,
funded: fundedEscrows,
released: releasedEscrows,
refunded: refundedEscrows,
disputed: disputedEscrows,
},
totalReleasedAmount: totalEscrowAmount._sum.amount ?? 0,
pendingAmount: pendingEscrowAmount._sum.amount ?? 0,
},
});
}

export async function getEarningsByPeriod(req: Request, res: Response): Promise<void> {
const authReq = req as AuthRequest;
const userId = authReq.user.userId;
const { startDate, endDate } = req.query;

const dateRange = getDateRange(
startDate as string | undefined,
endDate as string | undefined
);

const where: Record<string, unknown> = {
userId,
status: "SUCCESS",
};

if (startDate || endDate) {
where.createdAt = dateRange;
}

const earnings = await prisma.payment.aggregate({
where,
_sum: { amount: true },
_count: true,
_avg: { amount: true },
});

const escrowEarnings = await prisma.escrow.aggregate({
where: {
freelancerId: userId,
status: "RELEASED",
...(startDate || endDate ? { releasedAt: dateRange } : {}),
},
_sum: { amount: true },
_count: true,
});

res.json({
success: true,
data: {
paymentEarnings: {
total: earnings._sum.amount ?? 0,
count: earnings._count,
average: earnings._avg.amount ?? 0,
},
escrowEarnings: {
total: escrowEarnings._sum.amount ?? 0,
count: escrowEarnings._count,
},
combinedTotal:
Number(earnings._sum.amount ?? 0) + Number(escrowEarnings._sum.amount ?? 0),
},
});
}
Loading
Loading