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
34 changes: 33 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,46 @@ out/
# Debug
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Keys
*.pem

# Tools
.claude
.lhci_reports/
bundle-reports/
trend-data.json

# Package dist
packages/*/dist/
frontend/src/generated/
packages/contracts/src/generated/
packages/contracts/src/generated/

# Test snapshots & output
**/__snapshots__/
**/e2e/__snapshots__/
frontend/e2e/__snapshots__/
output.txt
**/output.txt
frontend/components/layout/output.txt
frontend/app/output.txt

# Turbo
.turbo/cookies/
.turbo/daemon/

# TypeScript
*.tsbuildinfo

# Playwright / test results
/playwright-report
/blob-report
/test-results
/.playwright
/playwright/.cache
/coverage

# Misc
test-write.txt
169 changes: 169 additions & 0 deletions backend/src/controllers/ComplianceController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/**
* ComplianceController.ts — Issue #597
*
* HTTP layer for compliance dashboard.
* Handles request/response only — no business logic here.
*/

import { Request, Response, NextFunction } from 'express';
import { BaseController } from './BaseController.js';
import { ComplianceService, JurisdictionCode, ComplianceMetricType, ComplianceAlertStatus } from '../services/complianceService.js';

export class ComplianceController extends BaseController {
constructor(private readonly complianceService: typeof ComplianceService) {
super();
}

getDashboard = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
await this.execute(req, res, next, async (req, res) => {
const jurisdiction = req.query.jurisdiction as JurisdictionCode | undefined;
const metrics = this.complianceService.getDashboardMetrics(jurisdiction);
const jurisdictionStatus = this.complianceService.getJurisdictionStatus();
const openAlerts = this.complianceService.getAlerts('open');

res.status(200).json({
success: true,
data: { metrics, jurisdictionStatus, openAlerts: openAlerts.slice(0, 10), generatedAt: new Date().toISOString() },
});
});
};

getMetrics = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
await this.execute(req, res, next, async (req, res) => {
const jurisdiction = (req.query.jurisdiction as JurisdictionCode) || 'GLOBAL';
const complianceMetrics = this.complianceService.getMetrics(jurisdiction);
res.status(200).json({ success: true, data: complianceMetrics, jurisdiction });
});
};

getJurisdictions = async (_req: Request, res: Response, next: NextFunction): Promise<void> => {
await this.execute(_req, res, next, async (_req, res) => {
const jurisdictionStatus = this.complianceService.getJurisdictionStatus();
res.status(200).json({ success: true, data: jurisdictionStatus });
});
};

getThresholds = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
await this.execute(req, res, next, async (req, res) => {
const jurisdiction = req.query.jurisdiction as JurisdictionCode | undefined;
const thresholds = this.complianceService.getThresholds(jurisdiction);
res.status(200).json({ success: true, data: thresholds });
});
};

updateThreshold = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
await this.execute(req, res, next, async (req, res) => {
const { jurisdiction, metric } = req.params;
const { warningLevel, criticalLevel } = req.body;
const updated = this.complianceService.updateThreshold(
jurisdiction as JurisdictionCode,
metric as ComplianceMetricType,
{ warningLevel, criticalLevel },
);
res.status(200).json({ success: true, data: updated });
});
};

getAlerts = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
await this.execute(req, res, next, async (req, res) => {
const status = req.query.status as ComplianceAlertStatus | undefined;
const jurisdiction = req.query.jurisdiction as JurisdictionCode | undefined;
const alertList = this.complianceService.getAlerts(status, jurisdiction);
res.status(200).json({ success: true, data: alertList, count: alertList.length });
});
};

evaluateThresholds = async (_req: Request, res: Response, next: NextFunction): Promise<void> => {
await this.execute(_req, res, next, async (_req, res) => {
const newAlerts = this.complianceService.evaluateThresholds();
res.status(200).json({
success: true,
data: newAlerts,
count: newAlerts.length,
message: `Evaluation complete. ${newAlerts.length} new alert(s) raised.`,
});
});
};

acknowledgeAlert = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
await this.execute(req, res, next, async (req, res) => {
const userId = String(req.body.userId || 'system');
const alert = this.complianceService.acknowledgeAlert(String(String(req.params.id)), userId);
res.status(200).json({ success: true, data: alert });
});
};

resolveAlert = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
await this.execute(req, res, next, async (req, res) => {
const userId = String(req.body.userId || 'system');
const alert = this.complianceService.resolveAlert(String(String(req.params.id)), userId);
res.status(200).json({ success: true, data: alert });
});
};

requestReport = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
await this.execute(req, res, next, async (req, res) => {
const { period, jurisdiction = 'GLOBAL' } = req.body;
if (!period) {
res.status(400).json({ success: false, error: { message: 'period is required (e.g. 2026-01)' } });
return;
}
const report = await this.complianceService.requestReport(period, jurisdiction as JurisdictionCode);
res.status(202).json({ success: true, data: report });
});
};

listReports = async (_req: Request, res: Response, next: NextFunction): Promise<void> => {
await this.execute(_req, res, next, async (_req, res) => {
const reportList = this.complianceService.listReports();
res.status(200).json({ success: true, data: reportList, count: reportList.length });
});
};

getReport = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
await this.execute(req, res, next, async (req, res) => {
const report = this.complianceService.getReport(String(req.params.id));
if (!report) {
res.status(404).json({ success: false, error: { message: 'Report not found' } });
return;
}
res.status(200).json({ success: true, data: report });
});
};

exportReportJSON = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
await this.execute(req, res, next, async (req, res) => {
const report = this.complianceService.getReport(String(req.params.id));
if (!report) {
res.status(404).json({ success: false, error: { message: 'Report not found' } });
return;
}
const json = this.complianceService.exportReportAsJSON(String(req.params.id));
const filename = `compliance-report-${report.jurisdiction}-${report.period}.json`;
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
res.setHeader('Content-Type', 'application/json');
res.status(200).send(json);
});
};

exportCSV = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
await this.execute(req, res, next, async (req, res) => {
const jurisdiction = (req.query.jurisdiction as JurisdictionCode) || 'GLOBAL';
const csv = this.complianceService.exportMetricsAsCSV(jurisdiction);
const filename = `compliance-metrics-${jurisdiction}-${new Date().toISOString().slice(0, 10)}.csv`;
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
res.status(200).send(csv);
});
};

getAuditTrail = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
await this.execute(req, res, next, async (req, res) => {
const entityType = req.query.entityType as string | undefined;
const jurisdiction = req.query.jurisdiction as JurisdictionCode | undefined;
const limit = Math.min(parseInt(String(req.query.limit || '100')), 500);
const entries = this.complianceService.getAuditTrail(entityType, jurisdiction, limit);
res.status(200).json({ success: true, data: entries, count: entries.length });
});
};
}
120 changes: 120 additions & 0 deletions backend/src/controllers/OnboardingController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* OnboardingController.ts — Issue #597
*
* HTTP layer for onboarding wizard API.
* Handles request/response only — no business logic here.
*/

import { Request, Response, NextFunction } from 'express';
import { BaseController } from './BaseController.js';
import { OnboardingService } from '../services/onboarding.js';
import { OnboardingAnalyticsService } from '../services/onboardingAnalytics.js';
import { AppError } from '../middleware/errorHandler.js';

export class OnboardingController extends BaseController {
constructor(
private readonly onboardingService: typeof OnboardingService,
private readonly analyticsService: typeof OnboardingAnalyticsService,
) {
super();
}

createOnboarding = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
await this.execute(req, res, next, async (req, res) => {
this.validateRequired(req.body, ['merchantId', 'businessName', 'businessType', 'contactEmail']);
const onboarding = await this.onboardingService.createOnboarding(req.body);
res.status(201).json({ success: true, data: onboarding });
});
};

getOnboarding = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
await this.execute(req, res, next, async (req, res) => {
const onboarding = await this.onboardingService.getOnboarding(String(req.params.id));
if (!onboarding) throw new AppError(404, 'Onboarding not found', 'ONBOARDING_NOT_FOUND');
res.status(200).json({ success: true, data: onboarding });
});
};

getByMerchant = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
await this.execute(req, res, next, async (req, res) => {
const onboarding = await this.onboardingService.getOnboardingByMerchant(String(req.params.merchantId));
if (!onboarding) throw new AppError(404, 'Onboarding not found for merchant', 'ONBOARDING_NOT_FOUND');
res.status(200).json({ success: true, data: onboarding });
});
};

updateTask = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
await this.execute(req, res, next, async (req, res) => {
const onboarding = await this.onboardingService.updateTask(String(req.params.id), req.body);
res.status(200).json({ success: true, data: onboarding });
});
};

submitDocument = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
await this.execute(req, res, next, async (req, res) => {
const onboarding = await this.onboardingService.submitDocument(String(req.params.id), req.body);
res.status(200).json({ success: true, data: onboarding });
});
};

skipTask = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
await this.execute(req, res, next, async (req, res) => {
const onboarding = await this.onboardingService.skipTask(String(req.params.id), req.body);
res.status(200).json({ success: true, data: onboarding });
});
};

submitForReview = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
await this.execute(req, res, next, async (req, res) => {
const onboarding = await this.onboardingService.submitForReview(String(req.params.id));
res.status(200).json({ success: true, data: onboarding });
});
};

adminReview = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
await this.execute(req, res, next, async (req, res) => {
const onboarding = await this.onboardingService.adminReview(req.body);
res.status(200).json({ success: true, data: onboarding });
});
};

listOnboardings = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
await this.execute(req, res, next, async (req, res) => {
const { status } = req.query as { status?: string };
const onboardings = await this.onboardingService.getAllOnboardings(status as any);
res.status(200).json({ success: true, data: onboardings, count: onboardings.length });
});
};

// ─── Analytics ────────────────────────────────────────────────────────────────

upsertAnalyticsSession = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
await this.execute(req, res, next, async (req, res) => {
const session = this.analyticsService.upsertSession(req.body);
res.status(200).json({ success: true, data: session });
});
};

getAnalyticsSummary = async (_req: Request, res: Response, next: NextFunction): Promise<void> => {
await this.execute(_req, res, next, async (_req, res) => {
const summary = this.analyticsService.getSummary();
res.status(200).json({ success: true, data: summary });
});
};

listAnalyticsSessions = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
await this.execute(req, res, next, async (req, res) => {
const { role, variant } = req.query as { role?: string; variant?: string };
const sessionList = this.analyticsService.listSessions({ role: role as any, variant: variant as any });
res.status(200).json({ success: true, data: sessionList, count: sessionList.length });
});
};

getAnalyticsSession = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
await this.execute(req, res, next, async (req, res) => {
const session = this.analyticsService.getSession(String(req.params.sessionId));
if (!session) throw new AppError(404, 'Session not found', 'SESSION_NOT_FOUND');
res.status(200).json({ success: true, data: session });
});
};
}
Loading