Skip to content
Merged
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
13 changes: 13 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ const {
// Routes
const leaseRoutes = require("./src/routes/leaseRoutes");
const ownerRoutes = require("./src/routes/ownerRoutes");
const kycRoutes = require("./src/routes/kycRoutes");
const sanctionsRoutes = require("./src/routes/sanctionsRoutes");
const evictionNoticeRoutes = require("./src/routes/evictionNoticeRoutes");
const vendorRoutes = require("./src/routes/vendorRoutes");
const taxRoutes = require("./src/routes/taxRoutes");
const propertyRoutes = require("./src/routes/propertyRoutes");

const { LeaseCacheService } = require("./src/services/LeaseCacheService");

/**
* Build authentication middleware for landlords and tenants.
Expand Down Expand Up @@ -119,12 +127,14 @@ function createApp(dependencies = {}) {
createConditionProofService({ store: createFileConditionProofStore() });
const depositGatekeeper =
dependencies.securityDepositService || createSecurityDepositLockService();
const leaseCacheService = dependencies.leaseCacheService || new LeaseCacheService(database);

// Inject for use in routes/controllers
app.locals.database = database;
app.locals.availabilityService = availabilityService;
app.locals.assetMetadataService = assetMetadataService;
app.locals.lateFeeService = lateFeeService;
app.locals.leaseCacheService = leaseCacheService;

// Middleware
app.use(cors());
Expand Down Expand Up @@ -165,6 +175,9 @@ function createApp(dependencies = {}) {
app.use('/api/kyc', kycRoutes);
app.use('/api/sanctions', sanctionsRoutes);
app.use('/api/eviction-notices', evictionNoticeRoutes);
app.use('/api/vendors', vendorRoutes);
app.use('/api/tax', taxRoutes);
app.use('/api/properties', propertyRoutes);
app.use('/api', createPaymentRoutes(database));

// --- Lease Renewal Routes ---
Expand Down
32 changes: 32 additions & 0 deletions src/controllers/LeaseController.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,38 @@ class LeaseController {
return res.status(500).json({ error: 'Internal server error while retrieving active leases.', details: error.message });
}
}

/**
* Get lease status, checking Redis cache first.
* @route GET /api/leases/:leaseId/status
*/
async getLeaseStatus(req, res) {
try {
const { leaseId } = req.params;
const cacheService = req.app.locals.leaseCacheService;

if (!cacheService) {
console.warn("[LeaseController] LeaseCacheService not found in app.locals.");
// Fallback to DB
const database = req.app.locals.database;
const lease = database.getLeaseById(leaseId);
return res.status(200).json({ success: true, data: lease });
}

const status = await cacheService.getLeaseStatus(leaseId);
if (!status) {
return res.status(404).json({ success: false, error: 'Lease not found' });
}

return res.status(200).json({
success: true,
data: status
});
} catch (error) {
console.error('[LeaseController] Error fetching lease status:', error);
return res.status(500).json({ success: false, error: 'Internal server error' });
}
}
}

module.exports = new LeaseController();
35 changes: 35 additions & 0 deletions src/controllers/PropertyController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
class PropertyController {
constructor(searchService) {
this.searchService = searchService;
}

async search(req, res) {
const filters = {
minPrice: req.query.minPrice ? parseFloat(req.query.minPrice) : undefined,
maxPrice: req.query.maxPrice ? parseFloat(req.query.maxPrice) : undefined,
location: req.query.location,
minScore: req.query.minScore ? parseInt(req.query.minScore) : undefined
};

try {
const results = await this.searchService.searchProperties(filters);
res.status(200).json({ success: true, ...results });
} catch (error) {
console.error('[PropertyController] Search error:', error);
res.status(500).json({ success: false, error: 'Search failed' });
}
}

async indexProperty(req, res) {
try {
const property = req.body;
await this.searchService.indexProperty(property);
res.status(201).json({ success: true, message: 'Property indexed' });
} catch (error) {
console.error('[PropertyController] Index error:', error);
res.status(500).json({ success: false, error: 'Indexing failed' });
}
}
}

module.exports = { PropertyController };
32 changes: 32 additions & 0 deletions src/controllers/TaxController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
class TaxController {
constructor(taxEstimatorService) {
this.taxEstimatorService = taxEstimatorService;
}

/**
* Generate tax deduction report.
*/
async generateReport(req, res) {
try {
const { landlordId, year } = req.query;
if (!landlordId || !year) {
return res.status(400).json({ success: false, error: 'landlordId and year are required' });
}

const report = this.taxEstimatorService.generateTaxDeductionReport(landlordId, parseInt(year));
res.status(200).json({
success: true,
data: report
});
} catch (error) {
console.error('[TaxController] Error generating report:', error);
res.status(500).json({
success: false,
error: 'Failed to generate tax report',
details: error.message
});
}
}
}

module.exports = { TaxController };
44 changes: 41 additions & 3 deletions src/controllers/VendorController.js
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,8 @@ class VendorController {
}

/**
* Close maintenance ticket and revoke all associated access
* Close maintenance ticket and revoke all associated access.
* Also triggers any authorized direct-drip payments.
*/
async closeTicketAndRevokeAccess(req, res) {
try {
Expand All @@ -383,12 +384,23 @@ class VendorController {
// Revoke all access grants for this ticket
const revokedGrants = this.vendorService.revokeAccessForClosedTicket(ticketId);

// --- Direct-Drip Integration (Issue #29) ---
// Trigger payment if authorized
let paymentResult = null;
try {
paymentResult = await this.vendorService.triggerVendorPaymentOnJobCompletion(ticketId);
} catch (payError) {
console.warn(`[VendorController] Direct-drip payment failed for ticket ${ticketId}:`, payError.message);
}

res.status(200).json({
success: true,
message: 'Ticket closed and all vendor access revoked',
message: 'Ticket closed, access revoked, and direct-drip processed',
data: {
ticket,
revokedGrantsCount: revokedGrants.length
revokedGrantsCount: revokedGrants.length,
paymentProcessed: !!paymentResult,
paymentData: paymentResult
}
});
} catch (error) {
Expand All @@ -400,6 +412,32 @@ class VendorController {
});
}
}

/**
* Authorize a direct-drip payment for a maintenance ticket.
*/
async authorizePayment(req, res) {
try {
const authData = req.body;
if (!authData.jobId || !authData.amount) {
return res.status(400).json({ success: false, error: 'jobId and amount are required' });
}

const auth = this.vendorService.authorizeVendorPayment(authData);
res.status(201).json({
success: true,
message: 'Vendor payment authorized successfully',
data: auth
});
} catch (error) {
console.error('[VendorController] Error authorizing payment:', error);
res.status(500).json({
success: false,
error: 'Failed to authorize payment',
details: error.message
});
}
}
}

module.exports = { VendorController };
Loading
Loading