Summary
CrimeReportsService was a god service (~500 lines) owning: CRUD, file upload/Cloudinary, heatmap analytics, statistics, geospatial queries, and nearby alert logic. The controller also imported CloudinaryService directly and contained file-processing business logic.
Deep module analysis
Three distinct responsibilities were bundled:
- Core CRUD — create, update, delete, find, paginate
- Analytics — heatmap aggregation, statistics, geospatial queries
- File processing — Cloudinary upload with rollback on partial failure
Resolution (already implemented — 6b4649b)
CrimeReportAnalyticsService (new)
Extracted getHeatmapData(), getStatistics(), getNearbyAlert() into a dedicated service. CrimeReportsService delegates to it — callers don't need to know which service handles analytics.
processAttachments() moved to service
Controller no longer imports CloudinaryService. Business logic (parallel upload, rollback on error) lives in the service layer where it belongs.
// Before — controller did file upload business logic
@Post()
async create(@UploadedFiles() files, @Body() dto, @Req() req) {
const urls = [];
for (const file of files ?? []) {
const res = await this.cloudinaryService.uploadImage(file.buffer);
urls.push(res.secure_url);
}
// ... manual rollback if create fails
}
// After — controller is a thin adapter
@Post()
async create(@UploadedFiles() files, @Body() dto, @Req() req) {
return this.crimeReportsService.create(dto, files, req.user.id);
}
Result: Controller reduced from ~370 → ~220 lines. Analytics isolated and independently testable.
Test plan
🤖 Generated with Claude Code
Summary
CrimeReportsServicewas a god service (~500 lines) owning: CRUD, file upload/Cloudinary, heatmap analytics, statistics, geospatial queries, and nearby alert logic. The controller also importedCloudinaryServicedirectly and contained file-processing business logic.Deep module analysis
Three distinct responsibilities were bundled:
Resolution (already implemented — 6b4649b)
CrimeReportAnalyticsService(new)Extracted
getHeatmapData(),getStatistics(),getNearbyAlert()into a dedicated service.CrimeReportsServicedelegates to it — callers don't need to know which service handles analytics.processAttachments()moved to serviceController no longer imports
CloudinaryService. Business logic (parallel upload, rollback on error) lives in the service layer where it belongs.Result: Controller reduced from ~370 → ~220 lines. Analytics isolated and independently testable.
Test plan
GET /crime-reports/heatmapreturns correct aggregated dataGET /crime-reports/statisticsreturns totalsGET /crime-reports/nearby?lat=&lng=returns alert levelPOST /crime-reportswith file attachments — files uploaded, URLs storedPOST /crime-reportswith file upload failure — rollback, no partial state🤖 Generated with Claude Code