Skip to content

Commit 3d5469f

Browse files
Merge pull request #62 from Agbasimere/feat/top-rated-owners-5
Feat: Implement Top Rated Owners Leaderboard #5
2 parents 2db9081 + 2d4ad3f commit 3d5469f

File tree

5 files changed

+132
-1
lines changed

5 files changed

+132
-1
lines changed

index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ require('dotenv').config();
22
const express = require('express');
33
const cors = require('cors');
44
const leaseRoutes = require('./src/routes/leaseRoutes');
5+
const ownerRoutes = require('./src/routes/ownerRoutes');
56

67
const app = express();
78
const port = process.env.PORT || 3000;
@@ -68,6 +69,7 @@ app.use(express.urlencoded({ extended: true, limit: '50mb' }));
6869

6970
// Routes
7071
app.use('/api/leases', leaseRoutes);
72+
app.use('/api/owners', ownerRoutes);
7173

7274
app.get('/', (req, res) => {
7375
res.json({
@@ -78,7 +80,8 @@ app.get('/', (req, res) => {
7880
contract_id: process.env.CONTRACT_ID || 'CAEGD57WVTVQSYWYB23AISBW334QO7WNA5XQ56S45GH6BP3D2AVHKUG4',
7981
endpoints: {
8082
upload_lease: 'POST /api/leases/upload',
81-
view_lease_handshake: 'GET /api/leases/:leaseCID/handshake'
83+
view_lease_handshake: 'GET /api/leases/:leaseCID/handshake',
84+
top_owners: 'GET /api/owners/top'
8285
}
8386
app.use(express.json());
8487
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));

src/controllers/OwnerController.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
const OwnerService = require('../services/OwnerService');
2+
3+
class OwnerController {
4+
/**
5+
* Retrieves the top-rated owners based on completed lease counts.
6+
* @route GET /api/owners/top
7+
*/
8+
async getTopRated(req, res) {
9+
try {
10+
const limit = parseInt(req.query.limit) || 10;
11+
const topOwners = await OwnerService.getTopRatedOwners(limit);
12+
13+
console.log(`[OwnerController] Found ${topOwners.length} top-rated owners.`);
14+
15+
return res.status(200).json({
16+
status: 'success',
17+
message: 'Top-rated owners retrieved successfully.',
18+
data: topOwners
19+
});
20+
} catch (error) {
21+
console.error('[OwnerController] Error fetching top owners:', error);
22+
return res.status(500).json({ error: 'Internal server error while retrieving top owners.', details: error.message });
23+
}
24+
}
25+
}
26+
27+
module.exports = new OwnerController();

src/routes/ownerRoutes.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const express = require('express');
2+
const router = express.Router();
3+
const OwnerController = require('../controllers/OwnerController');
4+
5+
/**
6+
* Route: GET /api/owners/top
7+
* Description: Retrieves a leaderboard of top property owners by completed rentals.
8+
*/
9+
router.get('/top', (req, res) => OwnerController.getTopRated(req, res));
10+
11+
module.exports = router;

src/services/OwnerService.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* OwnerService handles analytics and retrieval for property owners.
3+
* In a real-world scenario, this service would fetch historical data from a
4+
* blockchain indexer (like Soroban event logs) or a local database.
5+
*/
6+
7+
class OwnerService {
8+
constructor() {
9+
// Mock data representing a subset of Indexed Leases
10+
// Status: 'Draft', 'Active', 'Completed', 'Cancelled'
11+
this.leases = [
12+
{ id: 'lease-101', owner_id: 'owner-A', status: 'Completed', tenant_id: 'tenant-1' },
13+
{ id: 'lease-102', owner_id: 'owner-B', status: 'Completed', tenant_id: 'tenant-2' },
14+
{ id: 'lease-103', owner_id: 'owner-A', status: 'Completed', tenant_id: 'tenant-3' },
15+
{ id: 'lease-104', owner_id: 'owner-C', status: 'Completed', tenant_id: 'tenant-4' },
16+
{ id: 'lease-105', owner_id: 'owner-B', status: 'Active', tenant_id: 'tenant-5' },
17+
{ id: 'lease-106', owner_id: 'owner-A', status: 'Completed', tenant_id: 'tenant-6' },
18+
{ id: 'lease-107', owner_id: 'owner-D', status: 'Completed', tenant_id: 'tenant-7' },
19+
{ id: 'lease-108', owner_id: 'owner-B', status: 'Completed', tenant_id: 'tenant-8' },
20+
];
21+
22+
// Mock data for Owner profiles
23+
this.owners = {
24+
'owner-A': { id: 'owner-A', name: 'Alice Estate', public_key: 'G...A1' },
25+
'owner-B': { id: 'owner-B', name: 'Bob Properties', public_key: 'G...B2' },
26+
'owner-C': { id: 'owner-C', name: 'Charlie Homes', public_key: 'G...C3' },
27+
'owner-D': { id: 'owner-D', name: 'David Rentals', public_key: 'G...D4' },
28+
};
29+
}
30+
31+
/**
32+
* Retrieves a leaderboard of Owners with the most 'Completed' leases.
33+
* @param {number} limit - Maximum number of top owners to return.
34+
* @returns {Array} - List of top owners with their success count.
35+
*/
36+
async getTopRatedOwners(limit = 5) {
37+
// 1. Filter for completed leases only
38+
const completedLeases = this.leases.filter(lease => lease.status === 'Completed');
39+
40+
// 2. Count completions per owner
41+
const countsByOwner = {};
42+
completedLeases.forEach(lease => {
43+
countsByOwner[lease.owner_id] = (countsByOwner[lease.owner_id] || 0) + 1;
44+
});
45+
46+
// 3. Map to owner objects and sort
47+
const topRated = Object.keys(countsByOwner)
48+
.map(ownerId => ({
49+
...this.owners[ownerId],
50+
successful_rentals: countsByOwner[ownerId]
51+
}))
52+
.sort((a, b) => b.successful_rentals - a.successful_rentals) // Descending
53+
.slice(0, limit);
54+
55+
return topRated;
56+
}
57+
}
58+
59+
module.exports = new OwnerService();
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const request = require('supertest');
2+
const app = require('../../index');
3+
4+
describe('OwnerController', () => {
5+
describe('GET /api/owners/top', () => {
6+
it('should return 200 and a list of top-rated owners', async () => {
7+
const response = await request(app).get('/api/owners/top');
8+
9+
expect(response.status).toBe(200);
10+
expect(response.body.status).toBe('success');
11+
expect(Array.isArray(response.body.data)).toBe(true);
12+
13+
// Check sorting (descending)
14+
const owners = response.body.data;
15+
if (owners.length >= 2) {
16+
expect(owners[0].successful_rentals).toBeGreaterThanOrEqual(owners[1].successful_rentals);
17+
}
18+
19+
// Check specific mock data from OwnerService (Alice should be top with 3)
20+
const alice = owners.find(o => o.name === 'Alice Estate');
21+
expect(alice).toBeDefined();
22+
expect(alice.successful_rentals).toBe(3);
23+
});
24+
25+
it('should respect the limit query parameter', async () => {
26+
const response = await request(app).get('/api/owners/top?limit=2');
27+
expect(response.status).toBe(200);
28+
expect(response.body.data.length).toBeLessThanOrEqual(2);
29+
});
30+
});
31+
});

0 commit comments

Comments
 (0)