Skip to content
Open
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
24 changes: 24 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,19 @@ import { logger, type AppLogger } from "./observability/logger";
import { getMetricsContentType, MetricsRegistry } from "./observability/metrics";
import { createAuthRouter } from "./routes/auth.routes";
import { createInvoiceRouter } from "./routes/invoice.routes";
import { createSettlementRouter } from "./routes/settlement.routes";
import { createDashboardRouter } from "./routes/dashboard.routes";
import type { AuthService } from "./services/auth.service";
import type { InvoiceService } from "./services/invoice.service";
import type { SettlementService } from "./services/settlement.service";
import type { DashboardService } from "./services/dashboard.service";
import type { AppConfig } from "./config/env";

export interface AppDependencies {
authService: AuthService;
invoiceService?: InvoiceService;
settlementService?: SettlementService;
dashboardService?: DashboardService;
logger?: AppLogger;
metricsEnabled?: boolean;
metricsRegistry?: MetricsRegistry;
Expand Down Expand Up @@ -111,6 +117,8 @@ export function createRequestLifecycleTracker(): RequestLifecycleTracker {
export function createApp({
authService,
invoiceService,
settlementService,
dashboardService,
logger: appLogger = logger,
metricsEnabled = true,
metricsRegistry = new MetricsRegistry(),
Expand Down Expand Up @@ -182,6 +190,22 @@ export function createApp({
}));
}

// Add settlement routes if service is provided
if (settlementService) {
app.use("/api/v1/settlement", createSettlementRouter({
settlementService,
authService,
}));
}

// Add dashboard routes if service is provided
if (dashboardService) {
app.use("/api/v1/dashboard", createDashboardRouter({
dashboardService,
authService,
}));
}

app.use(notFoundMiddleware);
app.use(createErrorMiddleware(appLogger));
app.locals.requestLifecycleTracker = requestLifecycleTracker;
Expand Down
71 changes: 71 additions & 0 deletions src/controllers/dashboard.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type { Request, Response, NextFunction } from "express";
import type { DashboardService } from "../services/dashboard.service";
import { HttpError } from "../utils/http-error";
import { ServiceError } from "../utils/service-error";
import { UserType } from "../types/enums";

export function createDashboardController(dashboardService: DashboardService) {
return {
async getSellerDashboard(
req: Request,
res: Response,
next: NextFunction,
): Promise<void> {
try {
if (!req.user) {
throw new HttpError(401, "Authentication required");
}

// Only sellers can access seller dashboard
if (req.user.userType !== UserType.SELLER && req.user.userType !== UserType.BOTH) {
throw new HttpError(403, "Only sellers can access seller dashboard");
}

const metrics = await dashboardService.getSellerDashboard(req.user.id);

res.status(200).json({
success: true,
data: metrics,
});
} catch (error) {
if (error instanceof ServiceError) {
next(new HttpError(error.statusCode, error.message));
return;
}

next(error);
}
},

async getInvestorDashboard(
req: Request,
res: Response,
next: NextFunction,
): Promise<void> {
try {
if (!req.user) {
throw new HttpError(401, "Authentication required");
}

// Only investors can access investor dashboard
if (req.user.userType !== UserType.INVESTOR && req.user.userType !== UserType.BOTH) {
throw new HttpError(403, "Only investors can access investor dashboard");
}

const metrics = await dashboardService.getInvestorDashboard(req.user.id);

res.status(200).json({
success: true,
data: metrics,
});
} catch (error) {
if (error instanceof ServiceError) {
next(new HttpError(error.statusCode, error.message));
return;
}

next(error);
}
},
};
}
78 changes: 78 additions & 0 deletions src/controllers/settlement.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type { Request, Response, NextFunction } from "express";
import Joi from "joi";
import type { SettlementService } from "../services/settlement.service";
import { HttpError } from "../utils/http-error";
import { ServiceError } from "../utils/service-error";
import { UserType } from "../types/enums";

const settleInvoiceSchema = Joi.object({
paidAmount: Joi.string().regex(/^\d+(\.\d{1,4})?$/).required(),
stellarTxHash: Joi.string().length(64).optional(),
settledAt: Joi.date().iso().optional(),
});

export interface SettleInvoiceRequest extends Request {
params: {
id: string;
};
body: {
paidAmount: string;
stellarTxHash?: string;
settledAt?: string;
};
}

export function createSettlementController(settlementService: SettlementService) {
return {
async settleInvoice(
req: SettleInvoiceRequest,
res: Response,
next: NextFunction,
): Promise<void> {
try {
if (!req.user) {
throw new HttpError(401, "Authentication required");
}

// MVP: Only sellers can settle their own invoices
if (req.user.userType !== UserType.SELLER && req.user.userType !== UserType.BOTH) {
throw new HttpError(403, "Only sellers can settle invoices");
}

const { error, value } = settleInvoiceSchema.validate(req.body, {
abortEarly: false,
stripUnknown: true,
});

if (error) {
throw new HttpError(
400,
"Request validation failed.",
error.details.map((detail) => detail.message),
);
}

const { id: invoiceId } = req.params;

const result = await settlementService.settleInvoice({
invoiceId,
paidAmount: value.paidAmount,
stellarTxHash: value.stellarTxHash,
settledAt: value.settledAt ? new Date(value.settledAt) : undefined,
});

res.status(200).json({
success: true,
data: result,
});
} catch (error) {
if (error instanceof ServiceError) {
next(new HttpError(error.statusCode, error.message));
return;
}

next(error);
}
},
};
}
16 changes: 11 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { logger } from "./observability/logger";
import { createAuthService } from "./services/auth.service";
import { createIPFSService } from "./services/ipfs.service";
import { createInvoiceService } from "./services/invoice.service";
import { createSettlementService } from "./services/settlement.service";
import { createDashboardService } from "./services/dashboard.service";
import { createVerifyPaymentService } from "./services/stellar/verify-payment.service";
import { createReconcilePendingStellarStateWorker } from "./workers/reconcile-pending-stellar-state.worker";

Expand Down Expand Up @@ -44,10 +46,14 @@ export async function bootstrap(): Promise<ApplicationRuntime> {
const authService = createAuthService(dataSource, config);
const ipfsService = createIPFSService(config.ipfs);
const invoiceService = createInvoiceService(dataSource, ipfsService);
const settlementService = createSettlementService(dataSource);
const dashboardService = createDashboardService(dataSource);
const requestLifecycleTracker = createRequestLifecycleTracker();
const app = createApp({
authService,
invoiceService,
settlementService,
dashboardService,
logger,
metricsEnabled: config.observability.metricsEnabled,
http: {
Expand All @@ -72,11 +78,11 @@ export async function bootstrap(): Promise<ApplicationRuntime> {

const reconciliationWorker = config.reconciliation.enabled
? createReconcilePendingStellarStateWorker(
dataSource,
createVerifyPaymentService(dataSource, getPaymentVerificationConfig()),
config.reconciliation,
logger,
)
dataSource,
createVerifyPaymentService(dataSource, getPaymentVerificationConfig()),
config.reconciliation,
logger,
)
: null;

reconciliationWorker?.start();
Expand Down
27 changes: 27 additions & 0 deletions src/routes/dashboard.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Router } from "express";
import type { DashboardService } from "../services/dashboard.service";
import { createDashboardController } from "../controllers/dashboard.controller";
import { createAuthMiddleware } from "../middleware/auth.middleware";
import type { AuthService } from "../services/auth.service";

export interface DashboardRouterDependencies {
dashboardService: DashboardService;
authService: AuthService;
}

export function createDashboardRouter({
dashboardService,
authService,
}: DashboardRouterDependencies): Router {
const router = Router();
const controller = createDashboardController(dashboardService);
const authMiddleware = createAuthMiddleware(authService);

// GET /api/v1/dashboard/seller - Get seller dashboard metrics
router.get("/seller", authMiddleware, controller.getSellerDashboard);

// GET /api/v1/dashboard/investor - Get investor dashboard metrics
router.get("/investor", authMiddleware, controller.getInvestorDashboard);

return router;
}
24 changes: 24 additions & 0 deletions src/routes/settlement.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Router } from "express";
import type { SettlementService } from "../services/settlement.service";
import { createSettlementController } from "../controllers/settlement.controller";
import { createAuthMiddleware } from "../middleware/auth.middleware";
import type { AuthService } from "../services/auth.service";

export interface SettlementRouterDependencies {
settlementService: SettlementService;
authService: AuthService;
}

export function createSettlementRouter({
settlementService,
authService,
}: SettlementRouterDependencies): Router {
const router = Router();
const controller = createSettlementController(settlementService);
const authMiddleware = createAuthMiddleware(authService);

// POST /api/v1/settlement/:id - Settle an invoice
router.post("/:id", authMiddleware, controller.settleInvoice);

return router;
}
Loading
Loading