diff --git a/app/Http/Controllers/V2/Dashboard/ViewTreeRestorationGoalController.php b/app/Http/Controllers/V2/Dashboard/ViewTreeRestorationGoalController.php index c0a504fb9..1b401547e 100644 --- a/app/Http/Controllers/V2/Dashboard/ViewTreeRestorationGoalController.php +++ b/app/Http/Controllers/V2/Dashboard/ViewTreeRestorationGoalController.php @@ -2,174 +2,51 @@ namespace App\Http\Controllers\V2\Dashboard; -use App\Helpers\TerrafundDashboardQueryHelper; use App\Http\Controllers\Controller; -use App\Http\Resources\V2\Dashboard\ViewTreeRestorationGoalResource; -use App\Models\V2\Projects\Project; -use App\Models\V2\Projects\ProjectReport; -use App\Models\V2\Sites\Site; -use App\Models\V2\Sites\SiteReport; -use App\Models\V2\TreeSpecies\TreeSpecies; -use Carbon\Carbon; +use App\Http\Resources\DelayedJobResource; +use App\Jobs\Dashboard\RunTreeRestorationGoalJob; +use App\Models\DelayedJob; +use App\Models\Traits\HasCacheParameter; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Redis; class ViewTreeRestorationGoalController extends Controller { - public function __invoke(Request $request): JsonResponse - { - $query = $this->prepareProjectQuery($request); - $rawProjectIds = $this->getRawProjectIds($query); - $allProjectIds = $this->getAllProjectIds($rawProjectIds); - $siteIds = $this->getSiteIds($allProjectIds); - $distinctDates = $this->getDistinctDates($siteIds); - $latestDueDate = $this->getLatestDueDate($distinctDates); - - $forProfitProjectIds = $this->filterProjectIdsByType($rawProjectIds, 'for-profit-organization'); - $nonProfitProjectIds = $this->filterProjectIdsByType($rawProjectIds, 'non-profit-organization'); - $forProfitSiteIds = $this->getSiteIds($forProfitProjectIds); - $nonProfitSiteIds = $this->getSiteIds($nonProfitProjectIds); - - $forProfitTreeCount = $this->treeCountByDueDate($forProfitProjectIds); - $nonProfitTreeCount = $this->treeCountByDueDate($nonProfitProjectIds); - - $totalTreesGrownGoal = $query->sum('trees_grown_goal'); - - $treesUnderRestorationActualTotal = $this->treeCountPerPeriod($siteIds, $distinctDates, $totalTreesGrownGoal); - $treesUnderRestorationActualForProfit = $this->treeCountPerPeriod($forProfitSiteIds, $distinctDates, $totalTreesGrownGoal); - $treesUnderRestorationActualNonProfit = $this->treeCountPerPeriod($nonProfitSiteIds, $distinctDates, $totalTreesGrownGoal); - - $averageSurvivalRateTotal = $this->getAverageSurvival($allProjectIds); - $averageSurvivalRateForProfit = $this->getAverageSurvival($forProfitProjectIds); - $averageSurvivalRateNonProfit = $this->getAverageSurvival($nonProfitProjectIds); + use HasCacheParameter; - $result = [ - 'forProfitTreeCount' => (int) $forProfitTreeCount, - 'nonProfitTreeCount' => (int) $nonProfitTreeCount, - 'totalTreesGrownGoal' => (int) $totalTreesGrownGoal, - 'treesUnderRestorationActualTotal' => $treesUnderRestorationActualTotal, - 'treesUnderRestorationActualForProfit' => $treesUnderRestorationActualForProfit, - 'treesUnderRestorationActualNonProfit' => $treesUnderRestorationActualNonProfit, - 'averageSurvivalRateTotal' => floatval($averageSurvivalRateTotal), - 'averageSurvivalRateForProfit' => floatval($averageSurvivalRateForProfit), - 'averageSurvivalRateNonProfit' => floatval($averageSurvivalRateNonProfit), - ]; - - return new JsonResponse(ViewTreeRestorationGoalResource::make($result)); - } - - private function prepareProjectQuery(Request $request) - { - return TerrafundDashboardQueryHelper::buildQueryFromRequest($request); - } - - private function getRawProjectIds($query) - { - return $query - ->select('v2_projects.id', 'organisations.type') - ->get(); - } - - private function getAllProjectIds($projectIds) - { - return $projectIds->pluck('id')->toArray(); - } - - private function getSiteIds($projectIds) - { - return Site::whereIn('project_id', $projectIds)->whereIn('status', Site::$approvedStatuses)->pluck('id'); - } - - private function getDistinctDates($siteIds) - { - return SiteReport::selectRaw('YEAR(due_at) as year, MONTH(due_at) as month') - ->whereNotNull('due_at') - ->whereIn('site_id', $siteIds) - ->groupBy('year', 'month') - ->get() - ->toArray(); - } - - private function filterProjectIdsByType($projectIds, $type) - { - return collect($projectIds)->filter(function ($row) use ($type) { - return $row->type === $type; - })->pluck('id')->toArray(); - } - - private function treeCountByDueDate(array $projectIds) - { - $projects = Project::whereIn('id', $projectIds)->get(); - - return $projects->sum(function ($project) { - return $project->trees_planted_count; - }); - } - - private function treeCountPerPeriod($siteIds, $distinctDates, $totalTreesGrownGoal) + public function __invoke(Request $request): JsonResponse { - $treesUnderRestorationActual = []; - $totalAmount = 0; - - foreach ($distinctDates as $date) { - $year = $date['year']; - $month = $date['month']; - $treeSpeciesAmount = 0; - - $reports = SiteReport::whereIn('site_id', $siteIds) - ->whereNotIn('v2_site_reports.status', SiteReport::UNSUBMITTED_STATUSES) - ->whereYear('v2_site_reports.due_at', $year) - ->whereMonth('v2_site_reports.due_at', $month) - ->get(); - - foreach ($reports as $report) { - $treeSpeciesAmount += $report->treeSpecies()->where('collection', TreeSpecies::COLLECTION_PLANTED)->sum('amount'); + try { + $cacheParameter = $this->getParametersFromRequest($request); + $cacheValue = Redis::get('tree-restoration-goal-'.$cacheParameter); + + if (! $cacheValue) { + $frameworks = data_get($request, 'filter.programmes', []); + $landscapes = data_get($request, 'filter.landscapes', []); + $organisations = data_get($request, 'filter.organisations.type', []); + $country = data_get($request, 'filter.country', ''); + + $delayedJob = DelayedJob::create(); + $job = new RunTreeRestorationGoalJob( + $delayedJob->id, + $frameworks, + $landscapes, + $organisations, + $country, + $cacheParameter + ); + dispatch($job); + + return (new DelayedJobResource($delayedJob))->additional(['message' => 'Data for total-section-header is being processed']); + } else { + return response()->json(json_decode($cacheValue)); } + } catch (\Exception $e) { + Log::error('Error calculating tree restoration goal: ' . $e->getMessage()); - $totalAmount = $totalTreesGrownGoal; - - $formattedDate = Carbon::create($year, $month, 1); - - $treesUnderRestorationActual[] = [ - 'dueDate' => $formattedDate, - 'treeSpeciesAmount' => (int) $treeSpeciesAmount, - 'treeSpeciesPercentage' => 0, - ]; - } - - foreach ($treesUnderRestorationActual as &$treeData) { - $percentage = ($totalAmount != 0) ? ($treeData['treeSpeciesAmount'] / $totalAmount) * 100 : 0; - $treeData['treeSpeciesPercentage'] = floatval(number_format($percentage, 3)); + return response()->json(['error' => 'An error occurred while calculating tree restoration goal'], 500); } - - return $treesUnderRestorationActual; - } - - private function getLatestDueDate($distinctDates) - { - $latestYear = 0; - $latestMonth = 0; - - foreach ($distinctDates as $entry) { - $year = $entry['year']; - $month = $entry['month']; - - if ($year > $latestYear || ($year == $latestYear && $month > $latestMonth)) { - $latestYear = $year; - $latestMonth = $month; - } - } - - $latestDate = [ - 'year' => $latestYear, - 'month' => $latestMonth, - ]; - - return $latestDate; - } - - private function getAverageSurvival(array $projectIds) - { - return ProjectReport::isApproved()->whereIn('project_id', $projectIds)->avg('pct_survival_to_date'); } } diff --git a/app/Jobs/Dashboard/RunTreeRestorationGoalJob.php b/app/Jobs/Dashboard/RunTreeRestorationGoalJob.php new file mode 100644 index 000000000..ec6b11b0e --- /dev/null +++ b/app/Jobs/Dashboard/RunTreeRestorationGoalJob.php @@ -0,0 +1,88 @@ +delayed_job_id = $delayed_job_id; + $this->frameworks = $frameworks; + $this->landscapes = $landscapes; + $this->organisations = $organisations; + $this->country = $country; + $this->cacheParameter = $cacheParameter; + } + + public function handle(TreeRestorationGoalService $treeRestorationGoalService) + { + try { + $delayedJob = DelayedJob::findOrFail($this->delayed_job_id); + + $request = new Request([ + 'filter' => [ + 'country' => $this->country, + 'programmes' => $this->frameworks, + 'landscapes' => $this->landscapes, + 'organisations.type' => $this->organisations, + ], + ]); + + $response = $treeRestorationGoalService->calculateTreeRestorationGoal($request); + Redis::set('tree-restoration-goal-' . $this->cacheParameter, json_encode($response)); + + $delayedJob->update([ + 'status' => DelayedJob::STATUS_SUCCEEDED, + 'payload' => ['message' => 'Tree Restoration Goal calculation completed'], + 'status_code' => Response::HTTP_OK, + ]); + + } catch (Exception $e) { + Log::error('Error in RunTreeRestorationGoalJob: ' . $e->getMessage()); + + DelayedJob::where('id', $this->delayed_job_id)->update([ + 'status' => DelayedJob::STATUS_FAILED, + 'payload' => json_encode(['error' => $e->getMessage()]), + 'status_code' => Response::HTTP_INTERNAL_SERVER_ERROR, + ]); + } + } +} diff --git a/app/Services/Dashboard/TreeRestorationGoalService.php b/app/Services/Dashboard/TreeRestorationGoalService.php new file mode 100644 index 000000000..97cad4dff --- /dev/null +++ b/app/Services/Dashboard/TreeRestorationGoalService.php @@ -0,0 +1,141 @@ +getRawProjectIds($query); + $allProjectIds = $this->getAllProjectIds($rawProjectIds); + $siteIds = $this->getSiteIds($allProjectIds); + $distinctDates = $this->getDistinctDates($siteIds); + + $forProfitProjectIds = $this->filterProjectIdsByType($rawProjectIds, 'for-profit-organization'); + $nonProfitProjectIds = $this->filterProjectIdsByType($rawProjectIds, 'non-profit-organization'); + $forProfitSiteIds = $this->getSiteIds($forProfitProjectIds); + $nonProfitSiteIds = $this->getSiteIds($nonProfitProjectIds); + + $forProfitTreeCount = $this->treeCountByDueDate($forProfitProjectIds); + $nonProfitTreeCount = $this->treeCountByDueDate($nonProfitProjectIds); + $totalTreesGrownGoal = $query->sum('trees_grown_goal'); + + $treesUnderRestorationActualTotal = $this->treeCountPerPeriod($siteIds, $distinctDates, $totalTreesGrownGoal); + $treesUnderRestorationActualForProfit = $this->treeCountPerPeriod($forProfitSiteIds, $distinctDates, $totalTreesGrownGoal); + $treesUnderRestorationActualNonProfit = $this->treeCountPerPeriod($nonProfitSiteIds, $distinctDates, $totalTreesGrownGoal); + + $averageSurvivalRateTotal = $this->getAverageSurvival($allProjectIds); + $averageSurvivalRateForProfit = $this->getAverageSurvival($forProfitProjectIds); + $averageSurvivalRateNonProfit = $this->getAverageSurvival($nonProfitProjectIds); + Log::info('final'); + + return [ + 'forProfitTreeCount' => (int) $forProfitTreeCount, + 'nonProfitTreeCount' => (int) $nonProfitTreeCount, + 'totalTreesGrownGoal' => (int) $totalTreesGrownGoal, + 'treesUnderRestorationActualTotal' => $treesUnderRestorationActualTotal, + 'treesUnderRestorationActualForProfit' => $treesUnderRestorationActualForProfit, + 'treesUnderRestorationActualNonProfit' => $treesUnderRestorationActualNonProfit, + 'averageSurvivalRateTotal' => floatval($averageSurvivalRateTotal), + 'averageSurvivalRateForProfit' => floatval($averageSurvivalRateForProfit), + 'averageSurvivalRateNonProfit' => floatval($averageSurvivalRateNonProfit), + ]; + } + + private function getRawProjectIds($query) + { + return $query + ->select('v2_projects.id', 'organisations.type') + ->get(); + } + + private function getAllProjectIds($projectIds) + { + return $projectIds->pluck('id')->toArray(); + } + + private function getSiteIds($projectIds) + { + return Site::whereIn('project_id', $projectIds)->whereIn('status', Site::$approvedStatuses)->pluck('id'); + } + + private function getDistinctDates($siteIds) + { + return SiteReport::selectRaw('YEAR(due_at) as year, MONTH(due_at) as month') + ->whereNotNull('due_at') + ->whereIn('site_id', $siteIds) + ->groupBy('year', 'month') + ->get() + ->toArray(); + } + + private function filterProjectIdsByType($projectIds, $type) + { + return collect($projectIds)->filter(function ($row) use ($type) { + return $row->type === $type; + })->pluck('id')->toArray(); + } + + private function treeCountByDueDate(array $projectIds) + { + $projects = Project::whereIn('id', $projectIds)->get(); + + return $projects->sum(function ($project) { + return $project->trees_planted_count; + }); + } + + private function treeCountPerPeriod($siteIds, $distinctDates, $totalTreesGrownGoal) + { + $treesUnderRestorationActual = []; + $totalAmount = $totalTreesGrownGoal; + + foreach ($distinctDates as $date) { + $year = $date['year']; + $month = $date['month']; + $treeSpeciesAmount = 0; + + $reports = SiteReport::whereIn('site_id', $siteIds) + ->whereNotIn('v2_site_reports.status', SiteReport::UNSUBMITTED_STATUSES) + ->whereYear('v2_site_reports.due_at', $year) + ->whereMonth('v2_site_reports.due_at', $month) + ->get(); + + foreach ($reports as $report) { + $treeSpeciesAmount += $report->treeSpecies()->where('collection', TreeSpecies::COLLECTION_PLANTED)->sum('amount'); + } + + $formattedDate = Carbon::create($year, $month, 1); + + $treesUnderRestorationActual[] = [ + 'dueDate' => $formattedDate, + 'treeSpeciesAmount' => (int) $treeSpeciesAmount, + 'treeSpeciesPercentage' => 0, + ]; + } + + foreach ($treesUnderRestorationActual as &$treeData) { + $percentage = ($totalAmount != 0) ? ($treeData['treeSpeciesAmount'] / $totalAmount) * 100 : 0; + $treeData['treeSpeciesPercentage'] = floatval(number_format($percentage, 3)); + } + + return $treesUnderRestorationActual; + } + + private function getAverageSurvival(array $projectIds) + { + return ProjectReport::isApproved()->whereIn('project_id', $projectIds)->avg('pct_survival_to_date'); + } +}