Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 269
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
PresupuestosController
0.00% covered (danger)
0.00%
0 / 269
0.00% covered (danger)
0.00%
0 / 10
2862
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 syncByDate
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
20
 syncErrorBudgets
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
20
 syncBudgetsWorks
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
20
 syncById
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getCountFailedBudgets
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
42
 syncByIds
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 listResyncRuns
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
42
 getResyncSummary
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
90
 downloadSyncBudgetsWorks
0.00% covered (danger)
0.00%
0 / 94
0.00% covered (danger)
0.00%
0 / 1
240
1<?php
2
3namespace App\Http\Controllers;
4
5use App\Models\TblCompanies;
6use App\Models\TblG3WOrdersUpdateLogs;
7use App\Models\TblG3WResyncRuns;
8use App\Models\TblQuotations;
9use App\Services\GestionaService;
10use Illuminate\Http\JsonResponse;
11use Illuminate\Http\Request;
12use App\Services\PresupuestosService;
13use Illuminate\Support\Carbon;
14use Illuminate\Support\Facades\Log;
15use Symfony\Component\Process\Process;
16use App\Exceptions\AppException;
17
18class PresupuestosController extends Controller
19{
20    public function __construct(protected PresupuestosService $presupuestosService, protected GestionaService $gestionaService)
21    {
22    }
23
24    /**
25     * Sincroniza presupuestos a partir de una fecha.
26     *
27     * @return JsonResponse
28     */
29    public function syncByDate(Request $request)
30    {
31        $date = escapeshellarg((string) $request->input('fecha'));
32        $name = escapeshellarg((string) request()->header('Name'));
33        $region = escapeshellarg((string) request()->header('Region'));
34
35        if ($region === 'Catalunya') {
36            $region = 'Cataluña';
37        }
38
39        if ($this->gestionaService->getSyncStatus($region) === 1) {
40            $startCronDateTime = date('Y-m-d H:i:s');
41            $this->presupuestosService->updateLogs(['id' => 0, 'error' => 'Synchronization already in progress.'], 0, [], $startCronDateTime, 'System', $region);
42
43            return response()->json([
44                'success' => false,
45                'message' => 'Synchronization already in progress.',
46            ], 400);
47        }
48
49        try {
50            $phpBinary = '/usr/bin/php';
51
52            $artisanPath = escapeshellarg(base_path('artisan'));
53
54            $command = sprintf(
55                '%s %s quotations:sync %s %s %s > /dev/null 2>&1 &',
56                $phpBinary,
57                $artisanPath,
58                $date,
59                $name,
60                $region
61            );
62
63            exec($command, $output, $returnVar);
64            /*$comand = "cd /var/www/html && php artisan quotations:sync $date $name $region > /dev/null 2>&1 &";
65            exec($comand, $output, $returnVar);*/
66
67            return response()->json([
68                'success' => true,
69                'message' => 'Synchronization started in background.',
70            ]);
71        } catch (\Exception $e) {
72            report(AppException::fromException($e, 'SYNC_BY_DATE_EXCEPTION'));
73            $this->gestionaService->setSyncStatus(0, $region);
74            Log::channel('g3w')->error('Failed to start sync: '.$e->getMessage());
75
76            return response()->json([
77                'success' => false,
78                'message' => $e->getMessage(),
79            ], 500);
80        }
81    }
82
83    /**
84     * @return JsonResponse
85     */
86    public function syncErrorBudgets()
87    {
88        $name = escapeshellarg((string) request()->header('Name'));
89        $region = escapeshellarg((string) request()->header('Region'));
90
91        if ($region === 'Catalunya') {
92            $region = 'Cataluña';
93        }
94
95        if ($this->gestionaService->getSyncStatus($region) === 1) {
96            return response()->json([
97                'success' => false,
98                'message' => 'Synchronization already in progress.',
99            ], 400);
100        }
101
102        try {
103            $phpBinary = '/usr/bin/php';
104
105            $artisanPath = escapeshellarg(base_path('artisan'));
106
107            $command = sprintf(
108                '%s %s quotations:retry-failed %s %s > /dev/null 2>&1 &',
109                $phpBinary,
110                $artisanPath,
111                $name,
112                $region
113            );
114
115            exec($command, $output, $returnVar);
116            /*$comand = "cd /var/www/html && php artisan quotations:retry-failed $name $region > /dev/null 2>&1 &";
117            exec($comand, $output, $returnVar);*/
118
119            return response()->json([
120                'success' => true,
121                'message' => 'Synchronization started in background.',
122            ]);
123        } catch (\Exception $e) {
124            report(AppException::fromException($e, 'SYNC_ERROR_BUDGETS_EXCEPTION'));
125            $this->gestionaService->setSyncStatus(0, $region);
126            Log::channel('g3w')->error('Failed to start sync: '.$e->getMessage());
127
128            return response()->json([
129                'success' => false,
130                'message' => $e->getMessage(),
131            ], 500);
132        }
133
134    }
135
136    /**
137     * @return JsonResponse
138     */
139    public function syncBudgetsWorks()
140    {
141        $name = escapeshellarg((string) request()->header('Name'));
142        $region = escapeshellarg((string) request()->header('Region'));
143
144        if ($region === 'Catalunya') {
145            $region = 'Cataluña';
146        }
147
148        if ($this->gestionaService->getSyncStatus($region) === 1) {
149            return response()->json([
150                'success' => false,
151                'message' => 'Synchronization already in progress.',
152            ], 400);
153        }
154
155        try {
156            $phpBinary = '/usr/bin/php';
157
158            $artisanPath = escapeshellarg(base_path('artisan'));
159
160            $command = sprintf(
161                '%s %s quotations:sync-work %s %s > /dev/null 2>&1 &',
162                $phpBinary,
163                $artisanPath,
164                $name,
165                $region
166            );
167
168            exec($command, $output, $returnVar);
169            /*$comand = "cd /var/www/html && php artisan quotations:sync-work $name $region > /dev/null 2>&1 &";
170            exec($comand, $output, $returnVar);*/
171
172            return response()->json([
173                'success' => true,
174                'message' => 'Synchronization started in background.',
175            ]);
176        } catch (\Exception $e) {
177            report(AppException::fromException($e, 'SYNC_BUDGETS_WORKS_EXCEPTION'));
178            $this->gestionaService->setSyncStatus(0, $region);
179            Log::channel('g3w')->error('Failed to start sync: '.$e->getMessage());
180
181            return response()->json([
182                'success' => false,
183                'message' => $e->getMessage(),
184            ], 500);
185        }
186
187    }
188
189    /**
190     * Sincroniza un presupuesto por su ID.
191     *
192     * @param $id
193     * @return JsonResponse
194     */
195    public function syncById($id)
196    {
197        $region = urldecode((string) request()->header('Region'));
198        if($region === "Catalunya"){ $region = "Cataluña"; }
199        return response()->json($this->presupuestosService->syncById($id, $region));
200    }
201
202    /**
203     * Sincroniza un presupuesto por su ID.
204     *
205     * @param $id
206     * @return JsonResponse
207     */
208    public function getCountFailedBudgets()
209    {
210        $region = request()->header('Region');
211
212        if ($region === 'Catalunya' || $region === "'Catalunya'") {
213            $region = 'Cataluña';
214        }
215
216        $company = TblCompanies::where('region', $region)->first();
217
218        if (! $company) {
219            return response()->json([
220                'countWarnings' => 0,
221            ], 200);
222        }
223
224        $ids = TblG3WOrdersUpdateLogs::whereNotNull('sync_error_ids')
225            ->where('company_id', $company->company_id)
226            ->get(['sync_error_ids']);
227
228        $allSyncErrorIds = [];
229
230        foreach ($ids as $id) {
231            if (is_string($id->sync_error_ids)) {
232                $id->sync_error_ids = json_decode($id->sync_error_ids, true);
233                $allSyncErrorIds = array_merge($allSyncErrorIds, $id->sync_error_ids);
234            }
235        }
236
237        $allSyncErrorIds = array_unique($allSyncErrorIds);
238
239        return response()->json([
240            'count' => count($allSyncErrorIds),
241        ]);
242    }
243
244    public function syncByIds(Request $request)
245    {
246        $ids = $request['ids'];
247        $date = $request['date'];
248        $region = urldecode($request->header('Region'));
249        $user = urldecode($request->header('Name'));
250
251        if ($region === 'Catalunya') {
252            $region = 'Cataluña';
253        }
254
255        return response()->json($this->presupuestosService->syncByIds($ids, $region, $date, $user));
256    }
257
258    /**
259     * FIRE-977: List resync run logs (for monitoring UI).
260     */
261    public function listResyncRuns(Request $request)
262    {
263        try {
264            $query = TblG3WResyncRuns::query()->orderBy('run_at', 'desc');
265
266            if ($request->query('date')) {
267                $query->whereDate('run_at', $request->query('date'));
268            }
269
270            // Region filter: query param takes priority, then header
271            $region = $request->query('region') ?: $request->header('Region');
272            if ($region) {
273                $region = urldecode((string) $region);
274                if ($region === 'Catalunya') {
275                    $region = 'Cataluña';
276                }
277                $query->where('region', $region);
278            }
279
280            $runs = $query->limit(500)->get();
281
282            return response()->json(['message' => 'OK', 'data' => $runs]);
283        } catch (\Exception $e) {
284            return response()->json(['message' => 'KO', 'error' => $e->getMessage()], 500);
285        }
286    }
287
288    /**
289     * FIRE-977: Get resync summary (total failed, last run info).
290     */
291    public function getResyncSummary(Request $request)
292    {
293        try {
294            $region = urldecode((string) $request->header('Region', ''));
295
296            if ($region === 'Catalunya') {
297                $region = 'Cataluña';
298            }
299
300            // Count distinct failed IDs across all companies (optionally filtered by region)
301            $totalFailed = 0;
302            $companiesQuery = TblCompanies::query();
303            if ($region) {
304                $companiesQuery->where('region', $region);
305            }
306            $companies = $companiesQuery->get();
307
308            foreach ($companies as $company) {
309                $logs = TblG3WOrdersUpdateLogs::whereNotNull('sync_error_ids')
310                    ->where('company_id', $company->company_id)
311                    ->get(['sync_error_ids']);
312
313                $companyErrorIds = [];
314                foreach ($logs as $log) {
315                    if (is_string($log->sync_error_ids)) {
316                        $decoded = json_decode($log->sync_error_ids, true);
317                        if (is_array($decoded)) {
318                            $companyErrorIds = array_merge($companyErrorIds, $decoded);
319                        }
320                    }
321                }
322                $totalFailed += count(array_unique($companyErrorIds));
323            }
324
325            $lastRunQuery = TblG3WResyncRuns::orderBy('run_at', 'desc');
326            if ($region) {
327                $lastRunQuery->where('region', $region);
328            }
329            $lastRun = $lastRunQuery->first();
330
331            return response()->json([
332                'message' => 'OK',
333                'data' => [
334                    'total_failed' => $totalFailed,
335                    'last_run_at' => $lastRun?->run_at,
336                    'last_run_success' => $lastRun?->success_count ?? 0,
337                    'last_run_failed' => $lastRun?->failed_count ?? 0,
338                ],
339            ]);
340        } catch (\Exception $e) {
341            return response()->json(['message' => 'KO', 'error' => $e->getMessage()], 500);
342        }
343    }
344
345    public function downloadSyncBudgetsWorks()
346    {
347        try {
348            $region = urldecode((string) request()->header('Region'));
349
350            if ($region === 'Catalunya') {
351                $region = 'Cataluña';
352            }
353
354            $companyId = TblCompanies::where('region', $region)->first()->company_id;
355            $totalDays = 90;
356            $dataResponse = [];
357            $alreadyG3wBudgets = [];
358
359            for ($i = 0; $i < $totalDays; $i++) {
360                $currentDate = Carbon::today()->subDays($i);
361                $dateStr = $currentDate->format('Y-m-d');
362
363                $g3wBudgets = $this->gestionaService->getBudgetsByDay($dateStr, $region);
364                if (is_string($g3wBudgets)) {
365                    $g3wBudgets = json_decode($g3wBudgets, true);
366                }
367
368                $g3wBudgetIds = array_map(fn(array $item) => $item["ID"], $g3wBudgets);
369
370                $newG3wBudgetIds = array_diff($g3wBudgetIds, $alreadyG3wBudgets);
371                $alreadyG3wBudgets = array_merge($alreadyG3wBudgets, $newG3wBudgetIds);
372                $countG3wBudgets = count($newG3wBudgetIds);
373
374                $fstBudgetsQuery = ($companyId == 18 || $companyId == 22)
375                    ? TblQuotations::whereIn("company_id", [18, 22])
376                    : TblQuotations::where("company_id", $companyId);
377
378                $fstBudgets = $fstBudgetsQuery
379                    ->where(function($query): void {
380                        $query->where("sync_import", 1)
381                            ->orWhere("sync_import_edited", 1);
382                    })
383                    ->whereDate('created_at', $dateStr)
384                    ->pluck('internal_quote_id')
385                    ->toArray();
386
387                $countFstBudgets = count($fstBudgets);
388
389                $missingIds = array_diff($newG3wBudgetIds, $fstBudgets);
390
391                $existingIdsQuery = ($companyId == 18 || $companyId == 22)
392                    ? TblQuotations::where("company_id", $companyId)
393                    : TblQuotations::where("company_id", $companyId);
394
395                $existingIds = $existingIdsQuery
396                    ->where(function($query): void {
397                        $query->where("sync_import", 1)
398                            ->orWhere("sync_import_edited", 1);
399                    })
400                    ->whereIn('internal_quote_id', $missingIds)
401                    ->pluck('internal_quote_id')
402                    ->toArray();
403
404                $finalMissingIds = array_diff($missingIds, $existingIds);
405
406                $filteredFstBudgets = array_filter($fstBudgets, fn($value) => $value !== null);
407                $duplicatedFst = [];
408                if (!empty($filteredFstBudgets)) {
409                    $counts = array_count_values($filteredFstBudgets);
410                    $duplicatedFst = array_keys(array_filter($counts, fn($count) => $count > 1));
411                }
412
413                $deletedIds = array_diff($fstBudgets, $newG3wBudgetIds);
414                foreach ($deletedIds as $key => $id) {
415                    $deleted = $this->gestionaService->checkDeleted($id, $region);
416                    if (! $deleted) {
417                        unset($deletedIds[$key]);
418                    }
419                }
420
421                $g3wOrdersUpdateLogs = TblG3WOrdersUpdateLogs::where('company_id', $companyId)
422                    ->whereDate('ended_at', $dateStr)
423                    ->where('processed_by', 'System')
424                    ->get();
425
426                $syncs = $g3wOrdersUpdateLogs->count();
427
428                $syncErrorIds = $g3wOrdersUpdateLogs->flatMap(function ($item): array {
429                        $ids = $item->sync_error_ids;
430                        if (is_string($ids)) {
431                            $decoded = json_decode($ids, true);
432                            $ids = is_array($decoded) ? $decoded : explode(',', $ids);
433                        }
434                        return (array) $ids;
435                    })
436                    ->filter()
437                    ->unique()
438                    ->toArray();
439
440                $countSyncErrorIds = count(array_values(array_unique($syncErrorIds)));
441
442                $dataResponse[] = [
443                    'date' => $dateStr,
444                    'g3wBudgets' => array_values($newG3wBudgetIds),
445                    'countG3wBudgets' => $countG3wBudgets,
446                    'fstBudgets' => $fstBudgets,
447                    'countfstBudgets' => $countFstBudgets,
448                    'missingIds' => array_values($finalMissingIds),
449                    'duplicatedFst' => $duplicatedFst,
450                    'deletedIds' => array_values($deletedIds),
451                    'syncs' => $syncs,
452                    'syncErrorIds' => $syncErrorIds,
453                    'countSyncErrorIds' => $countSyncErrorIds,
454                ];
455            }
456
457            usort($dataResponse, fn(array $a, array $b) => strtotime($b['date']) - strtotime($a['date']));
458
459            return response(['data' => $dataResponse]);
460
461        } catch (\Exception $e) {
462            report(AppException::fromException($e, 'DOWNLOAD_SYNC_BUDGETS_WORKS_EXCEPTION'));
463            Log::error('Error en downloadSyncBudgetsWorks: ' . $e->getMessage());
464
465            if ($e->getMessage() == 'API URL is not defined.') {
466                return response()->json([
467                    'error' => 'KO',
468                    'message' => 'La conexión con la API de G3W no está configurada. Por favor, configúrala en los ajustes de la empresa.',
469                ], 200);
470            }
471
472            return response()->json([
473                'message' => 'Error interno del servidor',
474            ], 500);
475        }
476    }
477}