Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 89
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
ImportFinanceFromDrive
0.00% covered (danger)
0.00%
0 / 89
0.00% covered (danger)
0.00%
0 / 3
240
0.00% covered (danger)
0.00%
0 / 1
 handle
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
42
 buildDriveService
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 processFile
0.00% covered (danger)
0.00%
0 / 51
0.00% covered (danger)
0.00%
0 / 1
72
1<?php
2
3namespace App\Console\Commands;
4
5use App\Jobs\ImportFinanceDriveFile;
6use App\Models\TblFinanceReportSemanal;
7use App\Models\TblFinanceSedes;
8use Carbon\Carbon;
9use Google\Client as GoogleClient;
10use Google\Service\Drive as DriveService;
11use Google\Service\Drive\DriveFile;
12use Illuminate\Console\Command;
13use Illuminate\Support\Facades\Log;
14use PhpOffice\PhpSpreadsheet\IOFactory;
15
16class ImportFinanceFromDrive extends Command
17{
18    protected $signature = 'finance:import-drive';
19
20    protected $description = 'Import finance data from Excel files in Google Drive for regions not in Zenital';
21
22    /**
23     * Map of Spanish month names to month numbers.
24     */
25    private const MONTH_MAP = [
26        'enero' => 1,
27        'febrero' => 2,
28        'marzo' => 3,
29        'abril' => 4,
30        'mayo' => 5,
31        'junio' => 6,
32        'julio' => 7,
33        'agosto' => 8,
34        'septiembre' => 9,
35        'octubre' => 10,
36        'noviembre' => 11,
37        'diciembre' => 12,
38    ];
39
40    public function handle(): int
41    {
42        try {
43            $folderId = env('GOOGLE_DRIVE_FINANCE_FOLDER_ID') ?: env('GOOGLE_DRIVE_FOLDER_ID');
44            if (! $folderId) {
45                $this->error('GOOGLE_DRIVE_FINANCE_FOLDER_ID is not set.');
46
47                return 1;
48            }
49
50            $service = $this->buildDriveService();
51
52            // List children of the finance folder. Include Google Sheets in
53            // addition to .xlsx uploads — finance drops spreadsheets directly
54            // in Drive, which are google-apps.spreadsheet, not xlsx.
55            $list = $service->files->listFiles([
56                'q' => "'{$folderId}' in parents and trashed=false and ("
57                    ."mimeType='application/vnd.google-apps.spreadsheet' or "
58                    ."mimeType='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')",
59                'fields' => 'files(id,name,mimeType)',
60                'pageSize' => 200,
61                'supportsAllDrives' => true,
62                'includeItemsFromAllDrives' => true,
63            ]);
64
65            $files = $list->getFiles();
66            if (empty($files)) {
67                $this->info('No spreadsheet files found in Google Drive folder.');
68
69                return 0;
70            }
71
72            // FIRE-1151: dispatch one job per file. Under QUEUE_CONNECTION=sync
73            // (today) jobs run inline — same shape as the previous foreach.
74            // Under QUEUE_CONNECTION=redis + a worker, files process in parallel
75            // capped by the 'drive' rate-limiter.
76            // Files stay in place — business updates the same spreadsheets weekly.
77            // Re-import is idempotent via updateOrCreate on (company_id, sede_id,
78            // year, month, week_date) in processFile().
79            foreach ($files as $file) {
80                /** @var DriveFile $file */
81                ImportFinanceDriveFile::dispatch(
82                    $file->getId(),
83                    $file->getName(),
84                    $file->getMimeType(),
85                )->onQueue('drive');
86
87                $this->info("Dispatched: {$file->getName()}");
88            }
89
90            Log::channel('third-party')->info('finance:import-drive dispatched', [
91                'files' => count($files),
92            ]);
93
94            return 0;
95        } catch (\Exception $e) {
96            $this->error("Import failed: {$e->getMessage()}");
97            Log::channel('third-party')->error("finance:import-drive — {$e->getMessage()}");
98
99            return 1;
100        }
101    }
102
103    private function buildDriveService(): DriveService
104    {
105        $client = new GoogleClient;
106        $client->setClientId(env('GOOGLE_DRIVE_CLIENT_ID'));
107        $client->setClientSecret(env('GOOGLE_DRIVE_CLIENT_SECRET'));
108        $client->refreshToken(env('GOOGLE_DRIVE_REFRESH_TOKEN'));
109
110        return new DriveService($client);
111    }
112
113    /**
114     * Expected Excel format (per-sede billing file):
115     * Filename: "Facturación <SedeName>.xlsx"
116     * Column A: Rango Inferior (start date, e.g. 01/01/2026)
117     * Column B: Rango Superior (end date, e.g. 31/01/2026)
118     * Column C: Mes (month name in Spanish: Enero, Febrero, etc.)
119     * Column D: Base Imponible (billing amount)
120     *
121     * Each row is stored into tbl_finance_report_semanal using week_date = Rango Inferior.
122     */
123    public function processFile(string $path, string $filename): int
124    {
125        // Parse sede name from filename: "Facturación Cano Lopera.xlsx" -> "Cano Lopera"
126        // Also tolerate a timestamp prefix like "2026-04-20_144743_Facturación X"
127        // left behind by older versions of this command that renamed files.
128        $sedeName = $filename;
129        $sedeName = preg_replace('/\.xlsx$/i', '', $sedeName);
130        $sedeName = preg_replace('/^\d{4}-\d{2}-\d{2}_\d{6}_/', '', $sedeName);
131        $sedeName = preg_replace('/^Facturación\s+/iu', '', $sedeName);
132        $sedeName = trim($sedeName);
133
134        if (empty($sedeName)) {
135            $this->warn("Could not parse sede name from filename: {$filename}");
136
137            return 0;
138        }
139
140        // Look up sede by name (case-insensitive partial match)
141        $sede = TblFinanceSedes::where('is_active', 1)
142            ->whereRaw('LOWER(name) LIKE ?', ['%'.strtolower($sedeName).'%'])
143            ->first();
144
145        if (! $sede) {
146            $this->warn("Sede not found for name: {$sedeName} (filename: {$filename})");
147
148            return 0;
149        }
150
151        $spreadsheet = IOFactory::load($path);
152        $sheet = $spreadsheet->getActiveSheet();
153        $rows = $sheet->toArray(null, true, true, true);
154
155        // Skip header row
156        array_shift($rows);
157        $imported = 0;
158
159        foreach ($rows as $row) {
160            $rangoInferior = $row['A'] ?? null;
161            $rangoSuperior = $row['B'] ?? null;
162            $mesName = $row['C'] ?? null;
163            $baseImponible = $row['D'] ?? null;
164
165            if (! $rangoInferior || ! $mesName) {
166                continue;
167            }
168
169            // Parse dates (format: dd/mm/yyyy)
170            try {
171                $weekStart = Carbon::createFromFormat('d/m/Y', trim($rangoInferior));
172            } catch (\Exception $e) {
173                $this->warn("Invalid start date: {$rangoInferior}");
174
175                continue;
176            }
177
178            // Determine year from the start date
179            $year = $weekStart->year;
180
181            // Map Spanish month name to number
182            $monthNumber = self::MONTH_MAP[strtolower(trim($mesName))] ?? null;
183            if (! $monthNumber) {
184                $this->warn("Unknown month name: {$mesName}");
185
186                continue;
187            }
188
189            // Use week_date = Rango Inferior (the table has a single week_date column, not separate start/end)
190            TblFinanceReportSemanal::updateOrCreate(
191                [
192                    'company_id' => $sede->company_id,
193                    'sede_id' => $sede->id,
194                    'year' => $year,
195                    'month' => $monthNumber,
196                    'week_date' => $weekStart->toDateString(),
197                ],
198                [
199                    'actuals' => $baseImponible,
200                    'source' => 'google_drive',
201                    'loaded_at' => now(),
202                ]
203            );
204            $imported++;
205        }
206
207        return $imported;
208    }
209}