Skip to content

RFC: Extract CrimeReportAnalyticsService + slim controller (god service decomposition) #7

@Khoa-Dam

Description

@Khoa-Dam

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:

  1. Core CRUD — create, update, delete, find, paginate
  2. Analytics — heatmap aggregation, statistics, geospatial queries
  3. 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

  • GET /crime-reports/heatmap returns correct aggregated data
  • GET /crime-reports/statistics returns totals
  • GET /crime-reports/nearby?lat=&lng= returns alert level
  • POST /crime-reports with file attachments — files uploaded, URLs stored
  • POST /crime-reports with file upload failure — rollback, no partial state

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions