Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
1.20% covered (danger)
1.20%
1 / 83
25.00% covered (danger)
25.00%
1 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
QuotationsRetryFailed
1.20% covered (danger)
1.20%
1 / 83
25.00% covered (danger)
25.00%
1 / 4
446.25
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 handle
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 processRegion
0.00% covered (danger)
0.00%
0 / 60
0.00% covered (danger)
0.00%
0 / 1
132
 getFailedIds
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3namespace App\Console\Commands;
4
5use App\Models\TblCompanies;
6use App\Models\TblG3WOrdersUpdateLogs;
7use App\Models\TblG3WResyncRuns;
8use App\Models\TblQuotations;
9use App\Services\GestionaService;
10use App\Services\PresupuestosService;
11use Illuminate\Console\Command;
12use Illuminate\Support\Facades\Log;
13
14class QuotationsRetryFailed extends Command
15{
16    /**
17     * The name and signature of the console command.
18     *
19     * @var string
20     */
21    protected $signature = 'quotations:retry-failed {--region=} {--verbose-errors}';
22
23    /**
24     * The console command description.
25     *
26     * @var string
27     */
28    protected $description = 'Re-syncs failed and zero-amount quotations across all regions (FIRE-977)';
29
30    /**
31     * All active regions.
32     */
33    private const REGIONS = [
34        'Cataluña',
35        'Madrid',
36        'Comunidad Valenciana',
37        'Andalucía',
38        'Baleares',
39    ];
40
41    /**
42     * Active budget statuses to consider for zero-amount resync.
43     */
44    private const ACTIVE_STATUS_IDS = [1, 2, 3, 11, 17];
45
46    /**
47     * Create a new command instance.
48     */
49    public function __construct(
50        private readonly PresupuestosService $presupuestosService,
51        private readonly GestionaService $gestionaService,
52    ) {
53        parent::__construct();
54    }
55
56    /**
57     * Execute the console command.
58     */
59    public function handle(): int
60    {
61        $regionOption = $this->option('region');
62        $verbose = (bool) $this->option('verbose-errors');
63        $regions = $regionOption ? [$regionOption] : self::REGIONS;
64
65        $totalPending = 0;
66
67        foreach ($regions as $region) {
68            try {
69                $totalPending += $this->processRegion($region, $verbose);
70            } catch (\Exception $e) {
71                Log::channel('g3w')->error("quotations:retry-failed - Error processing region {$region}" . $e->getMessage());
72                $this->error("Error processing region {$region}" . $e->getMessage());
73            }
74        }
75
76        if ($totalPending === 0) {
77            $this->info('No pending quotations to resync across any region. Exiting.');
78        }
79
80        return Command::SUCCESS;
81    }
82
83    /**
84     * Process a single region: find all companies, gather failed + zero-amount IDs, resync.
85     *
86     * @param string $region
87     * @param bool   $verbose When true, print each failed ID + error message to CLI.
88     *
89     * @return int Total pending count for this region (across all companies)
90     */
91    private function processRegion(string $region, bool $verbose = false): int
92    {
93        $companies = TblCompanies::where('region', $region)->get();
94        $regionPendingTotal = 0;
95
96        foreach ($companies as $company) {
97            $companyId = $company->company_id;
98
99            // 1. Gather failed IDs from sync_error_ids logs
100            $failedIds = $this->getFailedIds($companyId);
101
102            // 2. Gather zero-amount quotation IDs (use internal_quote_id for G3W sync)
103            $zeroAmountQuotations = TblQuotations::where('company_id', $companyId)
104                ->where('sync_import', 1)
105                ->where(function ($q) {
106                    $q->where('amount', 0)->orWhereNull('amount');
107                })
108                ->whereIn('budget_status_id', self::ACTIVE_STATUS_IDS)
109                ->pluck('internal_quote_id')
110                ->filter()
111                ->toArray();
112
113            $zeroAmountCount = count($zeroAmountQuotations);
114
115            // 3. Merge both lists (unique)
116            $allIds = array_values(array_unique(array_merge($failedIds, $zeroAmountQuotations)));
117
118            if (empty($allIds)) {
119                continue;
120            }
121
122            // 4. Check sync lock — if running, skip this region (don't block)
123            if ($this->gestionaService->getSyncStatus($region) === 1) {
124                Log::channel('g3w')->info("quotations:retry-failed - Skipping region {$region} (company {$companyId}): sync already in progress.");
125                $this->warn("Skipping region {$region} (company {$companyId}): sync already in progress.");
126                continue;
127            }
128
129            $pendingCount = count($allIds);
130            $regionPendingTotal += $pendingCount;
131
132            $this->info("Region {$region}, company {$companyId}{$pendingCount} pending ({$zeroAmountCount} zero-amount)");
133
134            // 5. Resync each quotation
135            $successCount = 0;
136            $failedCount = 0;
137            $stillFailedIds = [];
138            $failedErrors = []; // map: id => error_message
139
140            foreach ($allIds as $id) {
141                $errorMessage = null;
142
143                try {
144                    $result = $this->presupuestosService->syncById($id, $region);
145
146                    if (!empty($result['success'])) {
147                        $successCount++;
148                    } else {
149                        $errorMessage = $result['error'] ?? 'Unknown error';
150                        $failedCount++;
151                        $stillFailedIds[] = $id;
152                        $failedErrors[(string) $id] = $errorMessage;
153                        Log::channel('g3w')->warning("quotations:retry-failed - Failed to resync ID {$id} in region {$region}" . $errorMessage);
154                    }
155                } catch (\Exception $e) {
156                    $errorMessage = $e->getMessage();
157                    $failedCount++;
158                    $stillFailedIds[] = $id;
159                    $failedErrors[(string) $id] = $errorMessage;
160                    Log::channel('g3w')->warning("quotations:retry-failed - Exception resyncing ID {$id} in region {$region}" . $errorMessage);
161                }
162
163                if ($verbose && $errorMessage !== null) {
164                    $this->line("    <fg=red>ID {$id}</>: {$errorMessage}");
165                }
166            }
167
168            // 6. Log run to tbl_g3w_resync_runs
169            TblG3WResyncRuns::create([
170                'company_id' => $companyId,
171                'region' => $region,
172                'run_at' => now(),
173                'pending_count' => $pendingCount,
174                'success_count' => $successCount,
175                'failed_count' => $failedCount,
176                'zero_amount_count' => $zeroAmountCount,
177                'failed_ids_json' => !empty($stillFailedIds) ? json_encode($stillFailedIds) : null,
178                'failed_errors_json' => !empty($failedErrors) ? json_encode($failedErrors, JSON_UNESCAPED_UNICODE) : null,
179            ]);
180
181            $this->info("  -> Done: {$successCount} success, {$failedCount} failed");
182        }
183
184        return $regionPendingTotal;
185    }
186
187    /**
188     * Get all unique failed sync IDs for a company from the update logs.
189     *
190     * @return array<int>
191     */
192    private function getFailedIds(int $companyId): array
193    {
194        $logs = TblG3WOrdersUpdateLogs::whereNotNull('sync_error_ids')
195            ->where('company_id', $companyId)
196            ->get(['sync_error_ids']);
197
198        $allIds = [];
199
200        foreach ($logs as $log) {
201            if (is_string($log->sync_error_ids)) {
202                $decoded = json_decode($log->sync_error_ids, true);
203                if (is_array($decoded)) {
204                    $allIds = array_merge($allIds, $decoded);
205                }
206            }
207        }
208
209        return array_values(array_unique($allIds));
210    }
211}