Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
CRAP
0.00% covered (danger)
0.00%
0 / 1
QuotationsCleanupZeroDates
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 1
 handle
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace App\Console\Commands;
4
5use App\Models\TblQuotations;
6use Illuminate\Console\Command;
7use Illuminate\Support\Facades\Log;
8
9/**
10 * FIRE-1025 follow-up to FIRE-1001, widened by FIRE-1024.
11 *
12 * Sweeps `tbl_quotations.acceptance_date` rows that hold any zero-date
13 * sentinel back to NULL once a day. Replaces the five in-line sweepers in
14 * PresupuestosService.php (lines 186, 710, 1083, 1391, 1980 pre-fix) that
15 * were running this UPDATE at the END of every G3W sync iteration.
16 * Concurrent G3W syncs caused the same UPDATE to fight for an exclusive
17 * lock on `idx_acceptance_date`, which surfaced as the deadlocks observed
18 * on 2026-04-27 (`SHOW ENGINE INNODB STATUS` had multiple "Searching rows
19 * for update" / "WE ROLL BACK TRANSACTION (1)" entries on this exact
20 * statement).
21 *
22 * Since the FIRE-1001 + FIRE-1024 fix in PresupuestosService normalises
23 * the G3W `aceptacion` field on every write (`null`, `''`, `'0000-00-00'`,
24 * `'0000/00/00'`, `'0000-00-00 00:00:00'`, whitespace → null before the
25 * row is INSERTed/UPDATEd), no NEW row can land in this state. This
26 * command exists only to clean up legacy rows. After a few clean runs it
27 * will become a no-op; keep it scheduled as a janitor against any future
28 * regression that might re-introduce zero-dates.
29 *
30 * FIRE-1024 widening: pre-fix the matcher was `where('acceptance_date',
31 * '0000-00-00 00:00:00')` — only the datetime form. Any leak that produced
32 * the date-only `'0000-00-00'` or the slash form `'0000/00/00'` survived
33 * the daily sweep. Now matches all three sentinels via `whereIn`, which
34 * still uses the `idx_acceptance_date` index.
35 */
36class QuotationsCleanupZeroDates extends Command
37{
38    protected $signature = 'quotations:cleanup-zero-dates';
39
40    protected $description = 'Sweep legacy tbl_quotations.acceptance_date zero-date rows to NULL (FIRE-1025 / FIRE-1024)';
41
42    /**
43     * All zero-date sentinels we accept as "this means null." Datetime form
44     * is what the legacy in-line sweepers chased; date-only and slash forms
45     * surface from older G3W payloads that the FIRE-1001 normaliser missed.
46     */
47    private const ZERO_DATE_SENTINELS = [
48        '0000-00-00 00:00:00',
49        '0000-00-00',
50        '0000/00/00',
51    ];
52
53    public function handle(): int
54    {
55        $count = TblQuotations::whereIn('acceptance_date', self::ZERO_DATE_SENTINELS)->count();
56
57        if ($count === 0) {
58            $this->info('quotations:cleanup-zero-dates — no zero-date rows found.');
59            Log::channel('g3w')->info('quotations:cleanup-zero-dates — 0 rows.');
60            return Command::SUCCESS;
61        }
62
63        $updated = TblQuotations::whereIn('acceptance_date', self::ZERO_DATE_SENTINELS)
64            ->update(['acceptance_date' => null]);
65
66        $this->info("quotations:cleanup-zero-dates — set {$updated} rows to NULL (out of {$count} matched).");
67        Log::channel('g3w')->info("quotations:cleanup-zero-dates — set {$updated} rows to NULL.");
68
69        return Command::SUCCESS;
70    }
71}