Skip to content

Commit df569b7

Browse files
committed
merge: resolve conflict in server.ts - keep both feature and main imports
2 parents 168e171 + 702020f commit df569b7

22 files changed

+848
-112
lines changed

wata-board-dapp/README_TESTING.md

Lines changed: 0 additions & 36 deletions
This file was deleted.

wata-board-dapp/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@
2020
"dotenv": "^16.4.5",
2121
"express": "^4.18.2",
2222
"cors": "^2.8.5",
23-
"helmet": "^7.0.0"
23+
"helmet": "^7.0.0",
24+
"ws": "^8.13.0"
2425
},
2526
"devDependencies": {
2627
"@types/jest": "^29.5.12",
2728
"@types/node": "^20.11.0",
2829
"@types/express": "^4.17.17",
2930
"@types/cors": "^2.8.13",
31+
"@types/ws": "^8.5.6",
3032
"@types/supertest": "^6.0.2",
3133
"jest": "^29.7.0",
3234
"supertest": "^6.3.4",
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { AnalyticsService } from '../services/analyticsService';
2+
3+
describe('AnalyticsService', () => {
4+
it('generates a consistent analytics report', () => {
5+
const report = AnalyticsService.generateReport('demo-user');
6+
expect(report.userId).toBe('demo-user');
7+
expect(report.totalSpendMonthly).toBeGreaterThan(0);
8+
expect(report.monthlyTrend).toHaveLength(6);
9+
expect(report.yearlyTrend).toHaveLength(5);
10+
expect(report.utilityUsageBreakdown).toHaveProperty('water');
11+
expect(report.predictiveInsight).toContain('Spending is expected');
12+
});
13+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { handleClientError } from '../middleware/errorHandler';
2+
3+
describe('Error Handler Middleware', () => {
4+
it('returns a success response when client errors are reported', () => {
5+
const req = {
6+
body: {
7+
message: 'Client crash',
8+
stack: 'Error stack'
9+
},
10+
path: '/api/client-errors',
11+
ip: '127.0.0.1',
12+
get: jest.fn().mockReturnValue('Mozilla/5.0')
13+
} as any;
14+
15+
const json = jest.fn();
16+
const res = {
17+
status: jest.fn().mockReturnValue({ json })
18+
} as any;
19+
20+
handleClientError(req, res);
21+
22+
expect(res.status).toHaveBeenCalledWith(202);
23+
expect(json).toHaveBeenCalledWith({ success: true, message: 'Client error logged' });
24+
});
25+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import request from 'supertest';
2+
import app from '../server';
3+
4+
describe('API Integration', () => {
5+
it('returns analytics report on /api/analytics/:userId', async () => {
6+
const response = await request(app).get('/api/analytics/test-user');
7+
expect(response.status).toBe(200);
8+
expect(response.body.userId).toBe('test-user');
9+
expect(response.body.totalSpendMonthly).toBeGreaterThan(0);
10+
});
11+
12+
it('returns transaction status on /api/transaction-status/:transactionId', async () => {
13+
const response = await request(app).get('/api/transaction-status/test-tx');
14+
expect(response.status).toBe(200);
15+
expect(response.body).toMatchObject({ success: true, transactionId: 'test-tx' });
16+
expect(['pending', 'confirming', 'confirmed', 'failed']).toContain(response.body.status);
17+
});
18+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { getTransactionStatus, updateTransactionStatus } from '../services/websocketService';
2+
3+
describe('WebSocket Transaction Status Store', () => {
4+
it('stores and retrieves transaction details correctly', () => {
5+
updateTransactionStatus('tx-123', 'confirming');
6+
expect(getTransactionStatus('tx-123')).toBe('confirming');
7+
});
8+
9+
it('returns pending for unknown transactions', () => {
10+
expect(getTransactionStatus('unknown-tx')).toBe('pending');
11+
});
12+
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import express from 'express';
2+
import logger from '../utils/logger';
3+
4+
export interface ClientErrorRequestBody {
5+
message: string;
6+
stack?: string;
7+
componentStack?: string;
8+
source?: string;
9+
url?: string;
10+
userAgent?: string;
11+
extra?: Record<string, unknown>;
12+
}
13+
14+
export const handleClientError = (req: express.Request, res: express.Response) => {
15+
const payload = req.body as ClientErrorRequestBody;
16+
17+
logger.error('Client error reported', {
18+
...payload,
19+
path: req.path,
20+
ip: req.ip,
21+
userAgent: req.get('user-agent')
22+
});
23+
24+
res.status(202).json({ success: true, message: 'Client error logged' });
25+
};
26+
27+
export const apiErrorHandler: express.ErrorRequestHandler = (err, req, res, next) => {
28+
if (res.headersSent) {
29+
return next(err);
30+
}
31+
32+
if (err?.name === 'UnauthorizedError') {
33+
return res.status(401).json({ success: false, error: 'Unauthorized' });
34+
}
35+
36+
if (err?.message === 'Not allowed by CORS') {
37+
return res.status(403).json({ success: false, error: 'CORS policy violation' });
38+
}
39+
40+
logger.error('Unhandled server error', {
41+
message: err?.message,
42+
stack: err?.stack,
43+
path: req.path,
44+
method: req.method,
45+
body: req.body
46+
});
47+
48+
res.status(500).json({ success: false, error: 'Internal server error' });
49+
};

wata-board-dapp/src/server.ts

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import { tieredRateLimiter } from './middleware/rateLimiter';
1313
import monitoringRoutes from './routes/monitoring';
1414
import upgradeRoutes from './routes/upgrade';
1515
import currencyRoutes from './routes/currency';
16+
import { handleClientError, apiErrorHandler } from './middleware/errorHandler';
17+
import { AnalyticsService } from './services/analyticsService';
18+
import { getTransactionStatus, startWebsocketService, updateTransactionStatus } from './services/websocketService';
1619

1720
// Load environment variables
1821
dotenv.config();
@@ -96,6 +99,9 @@ app.use(cors(corsOptions));
9699
app.use(express.json({ limit: '10mb' }));
97100
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
98101

102+
// Client-side error logging endpoint
103+
app.post('/api/client-errors', handleClientError);
104+
99105
// Request logging middleware
100106
app.use((req, res, next) => {
101107
logger.info('Incoming HTTP Request', {
@@ -197,6 +203,9 @@ app.post('/api/payment', async (req, res) => {
197203
res.set('X-Rate-Limit-Remaining', result.rateLimitInfo?.remainingRequests?.toString() || '0');
198204

199205
if (result.success) {
206+
if (result.transactionId) {
207+
updateTransactionStatus(result.transactionId, 'confirmed');
208+
}
200209
return res.status(200).json({
201210
success: true,
202211
transactionId: result.transactionId,
@@ -206,6 +215,9 @@ app.post('/api/payment', async (req, res) => {
206215
}
207216
});
208217
} else {
218+
if (result.transactionId) {
219+
updateTransactionStatus(result.transactionId, 'failed');
220+
}
209221
// Handle rate limit errors with appropriate status codes
210222
if (result.error?.includes('Rate limit exceeded')) {
211223
return res.status(429).json({
@@ -270,6 +282,44 @@ app.get('/api/rate-limit/:userId', (req, res) => {
270282
}
271283
});
272284

285+
/**
286+
* GET /api/analytics/:userId
287+
* Provide analytics insights for a user
288+
*/
289+
app.get('/api/analytics/:userId', (req, res) => {
290+
try {
291+
const { userId } = req.params;
292+
if (!userId) {
293+
return res.status(400).json({ success: false, error: 'User ID is required' });
294+
}
295+
296+
const analytics = AnalyticsService.generateReport(userId);
297+
return res.status(200).json(analytics);
298+
} catch (error) {
299+
logger.error('Analytics report generation failed', { error, userId: req.params.userId });
300+
return res.status(500).json({ success: false, error: 'Failed to generate analytics report' });
301+
}
302+
});
303+
304+
/**
305+
* GET /api/transaction-status/:transactionId
306+
* Return current transaction status for real-time updates.
307+
*/
308+
app.get('/api/transaction-status/:transactionId', (req, res) => {
309+
try {
310+
const { transactionId } = req.params;
311+
if (!transactionId) {
312+
return res.status(400).json({ success: false, error: 'Transaction ID is required' });
313+
}
314+
315+
const status = getTransactionStatus(transactionId);
316+
return res.status(200).json({ success: true, transactionId, status });
317+
} catch (error) {
318+
logger.error('Transaction status query failed', { error, transactionId: req.params.transactionId });
319+
return res.status(500).json({ success: false, error: 'Unable to retrieve transaction status' });
320+
}
321+
});
322+
273323
/**
274324
* GET /api/payment/:meterId
275325
* Get total paid amount for a meter
@@ -316,27 +366,7 @@ app.get('/api/payment/:meterId', async (req, res) => {
316366
});
317367

318368
// Error handling middleware
319-
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
320-
if (err.name === 'UnauthorizedError') {
321-
return res.status(401).json({
322-
success: false,
323-
error: 'Unauthorized'
324-
});
325-
}
326-
327-
if (err.message === 'Not allowed by CORS') {
328-
return res.status(403).json({
329-
success: false,
330-
error: 'CORS policy violation'
331-
});
332-
}
333-
334-
logger.error('Unhandled server error', { err, method: req.method, path: req.path });
335-
return res.status(500).json({
336-
success: false,
337-
error: 'Internal server error'
338-
});
339-
});
369+
app.use(apiErrorHandler);
340370

341371
// 404 handler
342372
app.use('*', (req, res) => {
@@ -424,6 +454,8 @@ function startServer() {
424454
});
425455
});
426456
}
457+
458+
startWebsocketService();
427459
}
428460

429461
startServer();
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
export interface AnalyticsTrendPoint {
2+
label: string;
3+
value: number;
4+
}
5+
6+
export interface AnalyticsReport {
7+
userId: string;
8+
totalSpendYearly: number;
9+
totalSpendMonthly: number;
10+
paymentsThisMonth: number;
11+
averagePayment: number;
12+
utilityUsageBreakdown: Record<string, number>;
13+
monthlyTrend: AnalyticsTrendPoint[];
14+
yearlyTrend: AnalyticsTrendPoint[];
15+
predictiveInsight: string;
16+
}
17+
18+
function normalizePercentage(value: number, max: number) {
19+
return Math.min(100, Math.max(0, Math.round((value / max) * 100)));
20+
}
21+
22+
export class AnalyticsService {
23+
static generateReport(userId: string): AnalyticsReport {
24+
const base = Math.max(1, userId.length * 7);
25+
const totalSpendMonthly = Number((base * 12.4).toFixed(2));
26+
const totalSpendYearly = Number((totalSpendMonthly * 12).toFixed(2));
27+
const paymentsThisMonth = Math.max(3, Math.round(base / 2));
28+
const averagePayment = Number((totalSpendMonthly / paymentsThisMonth).toFixed(2));
29+
30+
const monthlyTrend = Array.from({ length: 6 }, (_, idx) => ({
31+
label: `${6 - idx}m ago`,
32+
value: Number((totalSpendMonthly * (0.7 + idx * 0.05)).toFixed(2))
33+
})).reverse();
34+
35+
const yearlyTrend = Array.from({ length: 5 }, (_, idx) => ({
36+
label: `${new Date().getFullYear() - (4 - idx)}`,
37+
value: Number((totalSpendYearly * (0.78 + idx * 0.05)).toFixed(2))
38+
}));
39+
40+
const usageWeights = {
41+
water: normalizePercentage(base * 1.2, 20),
42+
electricity: normalizePercentage(base * 1.6, 25),
43+
waste: normalizePercentage(base * 0.9, 15),
44+
internet: normalizePercentage(base * 0.8, 15),
45+
gas: normalizePercentage(base * 1.1, 25)
46+
};
47+
48+
const totalUsage = Object.values(usageWeights).reduce((sum, value) => sum + value, 0) || 1;
49+
const utilityUsageBreakdown = Object.fromEntries(
50+
Object.entries(usageWeights).map(([key, value]) => [key, Math.round((value / totalUsage) * 100)])
51+
);
52+
53+
return {
54+
userId,
55+
totalSpendYearly,
56+
totalSpendMonthly,
57+
paymentsThisMonth,
58+
averagePayment,
59+
utilityUsageBreakdown,
60+
monthlyTrend,
61+
yearlyTrend,
62+
predictiveInsight: `Spending is expected to remain ${paymentsThisMonth > 8 ? 'stable' : 'moderate'} over the next quarter with optimization opportunities on utility usage.`
63+
};
64+
}
65+
}

0 commit comments

Comments
 (0)