Skip to content

Commit 8379b09

Browse files
Merge pull request #84 from Alhaji-naira/feature/core-protocol-enhancements
feat: implement Docker setup, Property IDs, Yield Generator, and Part…
2 parents cd5fbe4 + ae41a3f commit 8379b09

File tree

6 files changed

+225
-1
lines changed

6 files changed

+225
-1
lines changed

Dockerfile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
FROM node:18-alpine
2+
3+
WORKDIR /app
4+
5+
COPY package*.json ./
6+
7+
RUN npm install
8+
9+
COPY . .
10+
11+
EXPOSE 3000
12+
13+
CMD ["npm", "start"]

PROPERTY_METADATA_STANDARD.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Standardizing On-Chain Property IDs (LeaseFlow Protocol)
2+
3+
## Overview
4+
To ensure interoperability with future Real Estate Marketplaces on the Stellar network, LeaseFlow implements a universal "Property Asset Metadata" standard. This allows properties to be universally identified and described on-chain.
5+
6+
## On-Chain Representation
7+
Each property is represented as a distinct asset or within a Soroban smart contract state, associated with a unique IPFS CID containing the property metadata. The standard relies on a deterministic hashing of the property's physical location (coordinates + standardized address) combined with the owner's Stellar account to create a unique identifier.
8+
9+
## JSON Schema Standard (Stored on IPFS)
10+
11+
```json
12+
{
13+
"$schema": "http://json-schema.org/draft-07/schema#",
14+
"title": "PropertyAssetMetadata",
15+
"type": "object",
16+
"properties": {
17+
"propertyId": {
18+
"type": "string",
19+
"description": "Unique deterministic hash of the property"
20+
},
21+
"address": {
22+
"type": "object",
23+
"properties": {
24+
"street": { "type": "string" },
25+
"city": { "type": "string" },
26+
"stateProvince": { "type": "string" },
27+
"country": { "type": "string" },
28+
"postalCode": { "type": "string" }
29+
},
30+
"required": ["street", "city", "country"]
31+
},
32+
"specifications": {
33+
"type": "object",
34+
"properties": {
35+
"bedrooms": { "type": "number" },
36+
"bathrooms": { "type": "number" },
37+
"squareFootage": { "type": "number" },
38+
"zoning": { "type": "string" },
39+
"yearBuilt": { "type": "number" }
40+
},
41+
"required": ["bedrooms", "squareFootage", "zoning"]
42+
}
43+
},
44+
"required": ["propertyId", "address", "specifications"]
45+
}
46+
```
47+
48+
By agreeing on how to store "Bedrooms," "SqFt," and "Zoning" on-chain, we ensure that LeaseFlow is interoperable with any decentralized application built on Stellar.

docker-compose.yml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
version: '3.8'
2+
3+
services:
4+
api:
5+
build: .
6+
ports:
7+
- "3000:3000"
8+
environment:
9+
- NODE_ENV=development
10+
- PORT=3000
11+
- DATABASE_URL=postgresql://leaseflow:password@postgres:5432/leaseflow_db
12+
- REDIS_URL=redis://redis:6379
13+
- STELLAR_NETWORK=testnet
14+
- HORIZON_URL=http://stellar:8000
15+
depends_on:
16+
postgres:
17+
condition: service_healthy
18+
redis:
19+
condition: service_healthy
20+
volumes:
21+
- .:/app
22+
- /app/node_modules
23+
24+
postgres:
25+
image: postgres:14-alpine
26+
environment:
27+
- POSTGRES_USER=leaseflow
28+
- POSTGRES_PASSWORD=password
29+
- POSTGRES_DB=leaseflow_db
30+
ports:
31+
- "5432:5432"
32+
healthcheck:
33+
test: ["CMD-SHELL", "pg_isready -U leaseflow -d leaseflow_db"]
34+
interval: 5s
35+
timeout: 5s
36+
retries: 5
37+
volumes:
38+
- postgres_data:/var/lib/postgresql/data
39+
40+
redis:
41+
image: redis:6-alpine
42+
ports:
43+
- "6379:6379"
44+
healthcheck:
45+
test: ["CMD", "redis-cli", "ping"]
46+
interval: 5s
47+
timeout: 5s
48+
retries: 5
49+
50+
stellar:
51+
image: stellar/quickstart:testing
52+
command: --testnet --enable-soroban-rpc
53+
ports:
54+
- "8000:8000"
55+
- "11626:11626"
56+
- "11625:11625"
57+
volumes:
58+
- stellar_data:/opt/stellar
59+
60+
volumes:
61+
postgres_data:
62+
stellar_data:

services/rentPaymentTrackerService.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,28 @@ class RentPaymentTrackerService {
130130

131131
// Update the lease payment status if we matched a lease.
132132
if (leaseId) {
133-
this.database.updateLeasePaymentStatus(leaseId, 'paid', op.created_at);
133+
const expectedRent = parseFloat(lease.rent_amount) || 0;
134+
const paidAmount = parseFloat(op.amount);
135+
const previousBalance = parseFloat(lease.remaining_balance) || 0;
136+
137+
const totalDue = expectedRent + previousBalance;
138+
139+
if (paidAmount < totalDue) {
140+
const remainingBalance = totalDue - paidAmount;
141+
const lateFeePercentage = 0.05; // 5% late fee on unpaid portion
142+
const appliedLateFee = remainingBalance * lateFeePercentage;
143+
144+
this.database.updateLeasePaymentStatus(leaseId, 'partial', op.created_at);
145+
if (this.database.updateLeaseBalance) {
146+
this.database.updateLeaseBalance(leaseId, remainingBalance, appliedLateFee);
147+
}
148+
console.warn(`[Debt Alert] Lease ${leaseId} partially paid. Remaining: ${remainingBalance}, Late Fee: ${appliedLateFee}`);
149+
} else {
150+
this.database.updateLeasePaymentStatus(leaseId, 'paid', op.created_at);
151+
if (this.database.updateLeaseBalance) {
152+
this.database.updateLeaseBalance(leaseId, 0, 0);
153+
}
154+
}
134155
}
135156

136157
return 'recorded';

src/db/appDatabase.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ class AppDatabase {
6565
bedrooms INTEGER,
6666
bathrooms INTEGER,
6767
square_footage INTEGER,
68+
remaining_balance REAL DEFAULT 0,
69+
applied_late_fee REAL DEFAULT 0,
6870
created_at TEXT NOT NULL,
6971
updated_at TEXT NOT NULL
7072
);
@@ -452,6 +454,8 @@ class AppDatabase {
452454
end_date AS endDate,
453455
renewable,
454456
disputed,
457+
remaining_balance AS remainingBalance,
458+
applied_late_fee AS appliedLateFee,
455459
created_at AS createdAt,
456460
updated_at AS updatedAt
457461
FROM leases
@@ -484,6 +488,8 @@ class AppDatabase {
484488
disputed,
485489
tenant_account_id AS tenantAccountId,
486490
payment_status AS paymentStatus,
491+
remaining_balance AS remainingBalance,
492+
applied_late_fee AS appliedLateFee,
487493
last_payment_at AS lastPaymentAt,
488494
created_at AS createdAt,
489495
updated_at AS updatedAt
@@ -1247,6 +1253,8 @@ class AppDatabase {
12471253
renewable,
12481254
disputed,
12491255
payment_status AS paymentStatus,
1256+
remaining_balance AS remainingBalance,
1257+
applied_late_fee AS appliedLateFee,
12501258
last_payment_at AS lastPaymentAt,
12511259
created_at AS createdAt,
12521260
updated_at AS updatedAt
@@ -1261,6 +1269,26 @@ class AppDatabase {
12611269
return row ? normalizeLeaseRow(row) : null;
12621270
}
12631271

1272+
/**
1273+
* Update a lease's remaining balance and applied late fee.
1274+
*
1275+
* @param {string} leaseId Lease identifier.
1276+
* @param {number} remainingBalance The unpaid rent balance.
1277+
* @param {number} appliedLateFee The newly applied late fee.
1278+
* @returns {void}
1279+
*/
1280+
updateLeaseBalance(leaseId, remainingBalance, appliedLateFee) {
1281+
this.db
1282+
.prepare(
1283+
`UPDATE leases
1284+
SET remaining_balance = ?,
1285+
applied_late_fee = ?,
1286+
updated_at = ?
1287+
WHERE id = ?`,
1288+
)
1289+
.run(remainingBalance, appliedLateFee, new Date().toISOString(), leaseId);
1290+
}
1291+
12641292
/**
12651293
* Persist an uploaded utility bill and the computed tenant share.
12661294
*
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* YieldGeneratorService
3+
*
4+
* Moves security deposits from a "Static Escrow" to a "Lending Pool" on Stellar.
5+
* The interest earned is split between the tenant and the platform.
6+
*/
7+
8+
class YieldGeneratorService {
9+
constructor(stellarService, database) {
10+
this.stellarService = stellarService;
11+
this.database = database;
12+
this.lendingPoolAddress = process.env.STELLAR_LENDING_POOL_ADDRESS;
13+
}
14+
15+
/**
16+
* Moves the security deposit to the lending pool.
17+
* Requires explicit tenant consent.
18+
*/
19+
async depositToYieldPool(leaseId, amount, tenantConsent) {
20+
if (!tenantConsent) {
21+
throw new Error("Tenant consent required for yield generation");
22+
}
23+
24+
// Move from static escrow to lending pool
25+
const txHash = await this.stellarService.transferToPool(this.lendingPoolAddress, amount);
26+
27+
// Record yield deposit in database
28+
await this.database.recordYieldDeposit(leaseId, amount, txHash);
29+
30+
return { success: true, txHash };
31+
}
32+
33+
/**
34+
* Calculates the yield generated and splits it between tenant and platform.
35+
*/
36+
async calculateAndSplitYield(leaseId) {
37+
// Calculate total yield generated from the pool for this lease
38+
const yieldAmount = await this.stellarService.getYieldBalance(leaseId);
39+
40+
// Split: 80% to tenant, 20% to platform
41+
const tenantShare = yieldAmount * 0.8;
42+
const platformShare = yieldAmount * 0.2;
43+
44+
return {
45+
totalYield: yieldAmount,
46+
tenantShare,
47+
platformShare
48+
};
49+
}
50+
}
51+
52+
module.exports = { YieldGeneratorService };

0 commit comments

Comments
 (0)