Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.11% covered (danger)
0.11%
1 / 942
5.00% covered (danger)
5.00%
1 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
FacturasService
0.11% covered (danger)
0.11%
1 / 942
5.00% covered (danger)
5.00%
1 / 20
26322.51
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
 getInvoices
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
12
 loopInvoices
0.00% covered (danger)
0.00%
0 / 148
0.00% covered (danger)
0.00%
0 / 1
380
 loopNextRemindersInvoices
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
56
 loopNextRemindersClients
0.00% covered (danger)
0.00%
0 / 78
0.00% covered (danger)
0.00%
0 / 1
240
 sendInvoice
0.00% covered (danger)
0.00%
0 / 136
0.00% covered (danger)
0.00%
0 / 1
650
 getAllInvoices
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 getAllInvoicesExceptions
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 sendCyCInvoices
0.00% covered (danger)
0.00%
0 / 68
0.00% covered (danger)
0.00%
0 / 1
110
 setAllMonthAdministratorsInvoices
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 1
132
 sendAdministratorsInvoices
0.00% covered (danger)
0.00%
0 / 88
0.00% covered (danger)
0.00%
0 / 1
42
 checkClientHasFreshdeskTask
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 checkClientHasFreshdeskTaskAll
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
30
 sendCallCenterInvoices
0.00% covered (danger)
0.00%
0 / 78
0.00% covered (danger)
0.00%
0 / 1
240
 addToSheets
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
56
 getGoogleSheetsService
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
56
 writeToGoogleSheet
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
6
 handleGoogleAuthCallback
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
42
 getVencimientosFormateados
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 listCreditDaysOffered
0.00% covered (danger)
0.00%
0 / 77
0.00% covered (danger)
0.00%
0 / 1
240
1<?php
2
3namespace App\Services;
4
5use App\Models\TblCompanies;
6use App\Models\TblInvoiceAdministrators;
7use App\Models\TblInvoiceReminders;
8use App\Models\TblInvoiceRemindersEmailTemplate;
9use App\Models\TblInvoicesExceptions;
10use App\Models\TblInvoicesNextReminders;
11use Carbon\Carbon;
12use Google\Client;
13use Google\Service\Sheets;
14use Google\Service\Sheets\ValueRange;
15use Illuminate\Http\Request;
16use Illuminate\Support\Facades\Http;
17use Illuminate\Support\Facades\Log;
18use Mockery\Exception;
19use SendGrid\Mail\Attachment;
20use SendGrid\Mail\Mail;
21
22class FacturasService extends GestionaService
23{
24    public function __construct()
25    {
26        parent::__construct();
27    }
28
29    public function getInvoices($region = 'Cataluña'): array
30    {
31        try {
32            if (! TblCompanies::where('region', $region)->where('invoice_reminder_active', 1)->exists()) {
33                throw new Exception("Sincronizacion no activa para $region");
34            }
35
36            $today = date('Y-m-d');
37            $next10days = date('Y-m-d', strtotime('+10 days'));
38            $lastWeek = date('Y-m-d', strtotime('-1 week'));
39
40            $counter = 1;
41
42            $nextWeekInvoices = $this->request('get', 'factura/vence/'.$next10days, $region, []);
43            // $todayInvoices = $this->request('get', 'factura/vence/' . $today, $region, []);
44            $lastWeekInvoices = $this->request('get', 'factura/vence/'.$lastWeek, $region, []);
45
46            $resultNextWeekInvoices = $this->loopInvoices($nextWeekInvoices, 1, $region, $counter, $next10days);
47            /*$counter = $resultNextWeekInvoices["counter"] - 1;
48            if($counter >= 30){
49                return ['success' => true];
50            }*/
51
52            // $resultTodayInvoices = $this->loopInvoices($todayInvoices, 2, $region, $counter);
53            /*$counter = $resultTodayInvoices["counter"] - 1;
54            if($counter >= 30){
55                return ['success' => true];
56            }*/
57
58            $this->loopInvoices($lastWeekInvoices, 3, $region, $counter, $lastWeek);
59
60            $this->loopNextRemindersInvoices($region);
61
62            $this->loopNextRemindersClients($region, $next10days, $lastWeek);
63
64            return ['success' => true];
65        } catch (\Exception $e) {
66            Log::channel('g3w_invoices')->error($e->getMessage());
67            Log::error('Trace: '.$e->getTraceAsString());
68
69            return ['success' => false, 'error' => $e->getMessage()];
70        }
71    }
72
73    public function loopInvoices(array $invoices, $reminder_type, $region, $counter, $date = null): array
74    {
75        $senders = [];
76        foreach ($invoices['facturas'] as $invoice) {
77            // Continue if region is Comunidad Valenciana and the invoice starts with M
78            if (
79                $region == 'Comunidad Valenciana'
80                && str_starts_with((string) $invoice['ID'], 'M')
81            ) {
82                continue;
83            }
84
85            // Check if exist a next reminder and jump the loop then
86            $existNextReminderInvoice = TblInvoicesNextReminders::where('invoice_number', $invoice['ID'])
87                ->where('region', $region)
88                ->exists();
89
90            if ($existNextReminderInvoice) {
91                continue;
92            }
93
94            $dataInvoice = $this->request('get', 'factura/'.$invoice['ID'], $region, []);
95
96            if (! isset($dataInvoice['factura'])) {
97                continue;
98            }
99
100            if ($this->checkClientHasFreshdeskTask($dataInvoice['factura']['cod_cliente'], $region)) {
101                continue;
102            }
103
104            $cobrada = $dataInvoice['factura']['cobrada'];
105            $formaPago = $dataInvoice['factura']['forma_pago_factura'];
106
107            // Invoice already paied
108            // Invoice that the payment method is not "transferencia"
109            if (
110                $cobrada !== 'NO'
111                || stripos((string) $formaPago, 'tr') === false
112            ) {
113                continue;
114            }
115
116            $existNextReminderClient = TblInvoicesNextReminders::where('id_client', $dataInvoice['factura']['cod_cliente'])
117                ->where('region', $region)
118                ->exists();
119
120            if ($existNextReminderClient) {
121                $this->addToSheets($dataInvoice['factura']['cod_cliente'], $dataInvoice['factura']['n_factura'], $region);
122
123                TblInvoicesNextReminders::create([
124                    'id_client' => $dataInvoice['factura']['cod_cliente'],
125                    'region' => $region,
126                    'invoice_number' => $dataInvoice['factura']['n_factura'],
127                ]);
128
129                continue;
130            }
131
132            $dataClient = null;
133
134            if ($dataInvoice['factura']['cod_cliente']) {
135                $dataClient = $this->request('get', 'cliente/'.$dataInvoice['factura']['cod_cliente'], $region, []);
136            }
137
138            if ($dataClient['cliente']['tipo_cliente'] == 'Administrador') {
139                $codService = $dataInvoice['factura']['lineas'][0]['cod_servicio'];
140                $dataService = $this->request('get', 'servicio/'.$codService, $region, []);
141
142                TblInvoiceAdministrators::create([
143                    'invoice_number' => $dataInvoice['factura']['n_factura'],
144                    'region' => $region,
145                    'name' => $dataClient['cliente']['empresa'],
146                    'CIF' => $dataClient['cliente']['cliente_cif'],
147                    'email' => $dataClient['cliente']['email'],
148                    'service_name' => $dataService['servicio']['nombre_servicio'],
149                    'service_addres' => $dataService['servicio']['direccion'],
150                    'send_date' => $date,
151                    'expiration_date' => $date,
152                    'amount' => $dataInvoice['factura']['importe_total_factura'],
153                ]);
154
155                continue;
156            }
157
158            $exists = TblInvoicesExceptions::where('cif', $dataClient['cliente']['cliente_cif'])
159                ->orWhere('name', $dataClient['cliente']['empresa'])
160                ->orWhere('administrator', $dataClient['cliente']['empresa'])
161                ->orWhere('id_admin_g3w', $invoice['ID'])
162                ->orWhere('invoice_number', $dataInvoice['factura']['n_factura'])
163                ->exists();
164
165            if ($exists) {
166                continue;
167            }
168
169            $invoice_number = $dataInvoice['factura']['n_factura'] ?? null;
170            $client_name = $dataClient['cliente']['empresa'] ?? null;
171            $client_email = $dataClient['cliente']['email'] ?? $dataClient['cliente']['email_facturacion'] ?? null;
172            $cif = $dataClient['cliente']['cliente_cif'] ?? null;
173            $issued_date = $dataInvoice['factura']['fecha_emision'] ?? null;
174            $expiration_date = $date ?? $dataInvoice['factura']['vencimientos'][0]['fecha_vencimiento'];
175            $amount = $dataInvoice['factura']['importe_total_factura'] ?? null;
176            $collection_date = null;
177            $document = $dataInvoice['factura']['documento'] ?? null;
178            $senders[$cif][] = [
179                'invoice_number' => $invoice_number,
180                'client_name' => $client_name,
181                'client_email' => $client_email,
182                'issued_date' => $issued_date,
183                'expiration_date' => $expiration_date,
184                'document' => $document,
185                'amount' => $amount,
186                'reminder_type' => $reminder_type,
187                'cif' => $cif,
188                'collection_date' => $collection_date,
189                'region' => $region,
190            ];
191
192        }
193
194        if (config('app.env') === 'production') {
195            foreach ($senders as $cif => $invoicesGroup) {
196
197                $totalFacturas = count($invoicesGroup);
198
199                if ($totalFacturas === 1) {
200                    $resultSend = $this->sendInvoice(
201                        $invoicesGroup[0]['invoice_number'],
202                        $invoicesGroup[0]['client_name'],
203                        $invoicesGroup[0]['client_email'],
204                        $invoicesGroup[0]['issued_date'],
205                        $invoicesGroup[0]['expiration_date'],
206                        $invoicesGroup[0]['document'],
207                        $invoicesGroup[0]['amount'],
208                        $invoicesGroup[0]['reminder_type'],
209                        $region);
210
211                    if (! $resultSend['success']) {
212                        continue;
213                    }
214
215                    TblInvoiceReminders::create([
216                        'invoice_number' => $invoicesGroup[0]['invoice_number'],
217                        'client_name' => $invoicesGroup[0]['client_name'],
218                        'client_email' => $invoicesGroup[0]['client_email'],
219                        'cif' => $invoicesGroup[0]['cif'],
220                        'issued_date' => $invoicesGroup[0]['issued_date'],
221                        'expiration_date' => $invoicesGroup[0]['expiration_date'],
222                        'amount' => $invoicesGroup[0]['amount'],
223                        'collection_date' => $invoicesGroup[0]['collection_date'],
224                        'region' => $invoicesGroup[0]['region'],
225                        'reminder_type' => $invoicesGroup[0]['reminder_type'],
226                    ]);
227                } else {
228                    $table = "<table style='border-collapse: collapse; width: 100%; font-family: Arial, sans-serif;'>
229                        <tr>
230                            <th style='border: 1px solid #999; padding: 8px; background-color: #f2f2f2; text-align: left;'>Número Factura</th>
231                            <th style='border: 1px solid #999; padding: 8px; background-color: #f2f2f2; text-align: left;'>Nombre cliente de servicio</th>
232                            <th style='border: 1px solid #999; padding: 8px; background-color: #f2f2f2; text-align: left;'>Dirección del servicio</th>
233                            <th style='border: 1px solid #999; padding: 8px; background-color: #f2f2f2; text-align: left;'>Fecha de emisión</th>
234                            <th style='border: 1px solid #999; padding: 8px; background-color: #f2f2f2; text-align: left;'>Fecha de vencimiento</th>
235                            <th style='border: 1px solid #999; padding: 8px; background-color: #f2f2f2; text-align: left;'>Importe</th>
236                        </tr>";
237                    $documentsBase64 = [];
238
239                    foreach ($invoicesGroup as $invoice) {
240                        $invoiceG3W = $this->request('get', 'factura/'.$invoice['invoice_number'], $region, []);
241                        $invoiceData = $invoiceG3W['factura'];
242
243                        $codService = $invoiceData['lineas'][0]['cod_servicio'];
244                        $dataService = $this->request('get', 'servicio/'.$codService, $region, []);
245                        $dataService = $dataService['servicio'];
246
247                        $invoiceNumber = $invoice['invoice_number'];
248                        $table .= "<tr>
249                            <td class='invoice_number' style='border: 1px solid #999; padding: 8px;'>".$invoiceNumber."</td>
250                            <td class='invoice_service_name' style='border: 1px solid #999; padding: 8px;'>".$dataService['nombre_servicio']."</td>
251                            <td class='invoice_service_addres' style='border: 1px solid #999; padding: 8px;'>".$dataService['direccion']."</td>
252                            <td class='invoice_send_date' style='border: 1px solid #999; padding: 8px;'>".Carbon::now()->format('d-m-Y')."</td>
253                            <td class='invoice_expiration_date' style='border: 1px solid #999; padding: 8px;'>".$invoice['expiration_date']."</td>
254                            <td class='invoice_amount' style='border: 1px solid #999; padding: 8px;'>".$invoice['amount'].' €</td>
255                        </tr>';
256
257                        $documentsBase64[] = [
258                            'content' => $invoiceData['documento'],
259                            'filename' => "Factura_{$invoiceNumber}.pdf",
260                        ];
261                    }
262
263                    $table .= '</table>';
264
265                    $this->sendInvoice(
266                        $invoicesGroup[0]['invoice_number'],
267                        $invoicesGroup[0]['client_name'],
268                        $invoicesGroup[0]['client_email'],
269                        Carbon::now()->format('d-m-Y'),
270                        $invoicesGroup[0]['expiration_date'],
271                        $documentsBase64,
272                        $invoicesGroup[0]['amount'],
273                        $invoicesGroup[0]['reminder_type'] === '1' ? 5 : 6,
274                        $region,
275                        $table
276                    );
277
278                }
279
280                $counter++;
281
282            }
283        }
284
285        return [
286            'success' => true,
287            'counter' => $counter,
288        ];
289    }
290
291    private function loopNextRemindersInvoices($region): void
292    {
293        $nextReminders = TblInvoicesNextReminders::where('region', $region)
294            ->where('next_reminders', date('Y-m-d'))
295            ->pluck('invoice_number');
296
297        foreach ($nextReminders as $nextReminder) {
298            $dataInvoice = $this->request('get', 'factura/'.$nextReminder, $region, []);
299
300            if ($this->checkClientHasFreshdeskTask($dataInvoice['factura']['cod_cliente'], $region)) {
301                continue;
302            }
303
304            $cobrada = $dataInvoice['factura']['cobrada'];
305            $formaPago = $dataInvoice['factura']['forma_pago_factura'];
306
307            // Invoice already paied
308            // Invoice that the payment method is not "transferencia"
309            if (
310                $cobrada !== 'NO'
311                || stripos((string) $formaPago, 'tr') === false
312            ) {
313                continue;
314            }
315
316            $dataClient = $this->request('get', 'cliente/'.$dataInvoice['factura']['cod_cliente'], $region, []);
317
318            $invoice_number = $dataInvoice['factura']['n_factura'] ?? null;
319            $client_name = $dataClient['cliente']['empresa'] ?? null;
320            $client_email = $dataClient['cliente']['email'] ?? $dataClient['cliente']['email_facturacion'] ?? null;
321            $cif = $dataClient['cliente']['cliente_cif'] ?? null;
322            $issued_date = $dataInvoice['factura']['fecha_emision'] ?? null;
323            $expiration_date = $this->getVencimientosFormateados($dataInvoice);
324            $amount = $dataInvoice['factura']['importe_total_factura'] ?? null;
325            $collection_date = null;
326            $document = $dataInvoice['factura']['documento'] ?? null;
327
328            if (config('app.env') === 'production') {
329                $resultSend = $this->sendInvoice($invoice_number, $client_name, $client_email, $issued_date, $expiration_date, $document, $amount, 2, $region);
330
331                if (! $resultSend['success']) {
332                    continue;
333                }
334            }
335
336            TblInvoiceReminders::create([
337                'invoice_number' => $invoice_number,
338                'client_name' => $client_name,
339                'client_email' => $client_email,
340                'cif' => $cif,
341                'issued_date' => $issued_date,
342                'expiration_date' => $expiration_date,
343                'amount' => $amount,
344                'collection_date' => $collection_date,
345                'region' => $region,
346                'reminder_type' => 2,
347            ]);
348        }
349    }
350
351    private function loopNextRemindersClients($region, string $next10days, string $lastWeek): void
352    {
353        $diaNext10 = date('j', strtotime($next10days));
354        $diaLastWeek = date('j', strtotime($lastWeek));
355
356        $nextWeekInvoices = TblInvoicesNextReminders::where('payment_day', $diaNext10)->get()->pluck('invoice_number');
357        $lastWeekInvoices = TblInvoicesNextReminders::where('payment_day', $diaLastWeek)->get()->pluck('invoice_number');
358
359        // type 1
360        foreach ($nextWeekInvoices as $reminder) {
361            if (! $reminder) {
362                continue;
363            }
364            $dataInvoice = $this->request('get', 'factura/'.$reminder, $region, []);
365            if ($this->checkClientHasFreshdeskTask($dataInvoice['factura']['cod_cliente'], $region)) {
366                continue;
367            }
368
369            $cobrada = $dataInvoice['factura']['cobrada'];
370            $formaPago = $dataInvoice['factura']['forma_pago_factura'];
371
372            // Invoice already paied
373            // Invoice that the payment method is not "transferencia"
374            if (
375                $cobrada !== 'NO'
376                || stripos((string) $formaPago, 'tr') === false
377            ) {
378                continue;
379            }
380
381            $dataClient = $this->request('get', 'cliente/'.$dataInvoice['factura']['cod_cliente'], $region, []);
382
383            $invoice_number = $dataInvoice['factura']['n_factura'] ?? null;
384            $client_name = $dataClient['cliente']['empresa'] ?? null;
385            $client_email = $dataClient['cliente']['email'] ?? $dataClient['cliente']['email_facturacion'] ?? null;
386            $cif = $dataClient['cliente']['cliente_cif'] ?? null;
387            $issued_date = $dataInvoice['factura']['fecha_emision'] ?? null;
388            $expiration_date = $this->getVencimientosFormateados($dataInvoice);
389            $amount = $dataInvoice['factura']['importe_total_factura'] ?? null;
390            $collection_date = null;
391            $document = $dataInvoice['factura']['documento'] ?? null;
392
393            if (config('app.env') === 'production') {
394                $resultSend = $this->sendInvoice($invoice_number, $client_name, $client_email, $issued_date, $expiration_date, $document, $amount, 1, $region);
395
396                if (! $resultSend['success']) {
397                    continue;
398                }
399            }
400
401            TblInvoiceReminders::create([
402                'invoice_number' => $invoice_number,
403                'client_name' => $client_name,
404                'client_email' => $client_email,
405                'cif' => $cif,
406                'issued_date' => $issued_date,
407                'expiration_date' => $expiration_date,
408                'amount' => $amount,
409                'collection_date' => $collection_date,
410                'region' => $region,
411                'reminder_type' => 1,
412            ]);
413        }
414
415        // type 3
416        foreach ($lastWeekInvoices as $reminder) {
417            if (! $reminder) {
418                continue;
419            }
420            $dataInvoice = $this->request('get', 'factura/'.$reminder, $region, []);
421            if ($this->checkClientHasFreshdeskTask($dataInvoice['factura']['cod_cliente'], $region)) {
422                continue;
423            }
424
425            $cobrada = $dataInvoice['factura']['cobrada'];
426            $formaPago = $dataInvoice['factura']['forma_pago_factura'];
427
428            // Invoice already paied
429            // Invoice that the payment method is not "transferencia"
430            if (
431                $cobrada !== 'NO'
432                || stripos((string) $formaPago, 'tr') === false
433            ) {
434                continue;
435            }
436
437            $dataClient = $this->request('get', 'cliente/'.$dataInvoice['factura']['cod_cliente'], $region, []);
438
439            $invoice_number = $dataInvoice['factura']['n_factura'] ?? null;
440            $client_name = $dataClient['cliente']['empresa'] ?? null;
441            $client_email = $dataClient['cliente']['email'] ?? $dataClient['cliente']['email_facturacion'] ?? null;
442            $cif = $dataClient['cliente']['cliente_cif'] ?? null;
443            $issued_date = $dataInvoice['factura']['fecha_emision'] ?? null;
444            $expiration_date = $this->getVencimientosFormateados($dataInvoice);
445            $amount = $dataInvoice['factura']['importe_total_factura'] ?? null;
446            $collection_date = null;
447            $document = $dataInvoice['factura']['documento'] ?? null;
448
449            if (config('app.env') === 'production') {
450                $resultSend = $this->sendInvoice($invoice_number, $client_name, $client_email, $issued_date, $expiration_date, $document, $amount, 3, $region);
451
452                if (! $resultSend['success']) {
453                    continue;
454                }
455            }
456
457            TblInvoiceReminders::create([
458                'invoice_number' => $invoice_number,
459                'client_name' => $client_name,
460                'client_email' => $client_email,
461                'cif' => $cif,
462                'issued_date' => $issued_date,
463                'expiration_date' => $expiration_date,
464                'amount' => $amount,
465                'collection_date' => $collection_date,
466                'region' => $region,
467                'reminder_type' => 3,
468            ]);
469        }
470
471    }
472
473    public function sendInvoice($invoice_number, $client_name, $client_email, $issued_date, $expiration_date, $document, $amount, $reminder_type, $region, $table = null, $month = null): array
474    {
475        if (
476            empty($invoice_number) ||
477            empty($client_name) ||
478            empty($client_email) ||
479            empty($issued_date) ||
480            empty($expiration_date) ||
481            empty($document) ||
482            empty($reminder_type)
483        ) {
484            Log::channel('g3w_invoices_not_send')->error("$invoice_number => {
485            'client_name': $client_name,
486            'client_email': $client_email,
487            'issued_date': $issued_date,
488            'expiration_date': $expiration_date,
489            'reminder_type': $reminder_type
490        }");
491
492            return ['success' => false, 'message' => 'Campos obligatorios faltantes'];
493        }
494
495        try {
496            $emailTemplate = TblInvoiceRemindersEmailTemplate::where('type', $reminder_type)->first();
497
498            if (! $emailTemplate) {
499                throw new \Exception('No se encontró la plantilla de correo para este tipo de recordatorio');
500            }
501
502            $clientEmails = preg_split('/\s*[,;\-\s]\s*/', (string) $client_email, -1, PREG_SPLIT_NO_EMPTY);
503            $clientEmails = array_map(trim(...), $clientEmails);
504            $validEmails = [];
505
506            foreach ($clientEmails as $email) {
507                if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
508                    $validEmails[] = $email;
509                } else {
510                    Log::channel('g3w_invoices_not_send')->warning("Email inválido omitido: $email para factura $invoice_number");
511                }
512            }
513
514            if (empty($validEmails)) {
515                Log::channel('g3w_invoices_not_send')->error("No hay emails válidos para enviar factura $invoice_number");
516
517                return ['success' => false, 'message' => 'No hay direcciones de correo válidas'];
518            }
519
520            $sendgrid = new \SendGrid(config('services.sendgrid.api_key'));
521            $successCount = 0;
522            $failCount = 0;
523
524            foreach ($validEmails as $toEmail) {
525                try {
526                    $email = new Mail;
527
528                    $fromEmail = 'recordatorio.factura@fire.es';
529                    $fromName = 'Recordatorio Facturas Grupo Fire';
530                    $email->setFrom($fromEmail, $fromName);
531                    $email->setReplyTo('recordatorio.facturas@fire.es', 'Recordatorio Facturas Grupo Fire');
532
533                    if ($reminder_type != 4) {
534                        $subject = str_replace(
535                            // ['{{invoice_number}}', '{{expiration_date}}', '{{amount}}'],
536                            ['{{invoice_number}}', '{{expiration_date}}'],
537                            [$invoice_number, $expiration_date, $amount],
538                            $emailTemplate->subject
539                        );
540                    } else {
541                        $subject = str_replace(
542                            // ['{{invoice_number}}', '{{expiration_date}}', '{{amount}}'],
543                            ['{{expiration_date}}'],
544                            [$month],
545                            $emailTemplate->subject
546                        );
547                    }
548
549                    $baseUrl = 'http://aiwf.fire.es/sepa/form/';
550
551                    $params = [
552                        'nombre_deudor' => $client_name,
553                        // 'cif_deudor' => $datos['cif'],
554                        // 'direccion_deudor' => urlencode($datos['direccion_deudor']),
555                        // 'codigo_postal' => urlencode($datos['codigo_postal']),
556                        // 'provincia_pais' => urlencode($datos['provincia_pais']),
557                        // 'iban' => $datos['iban'],
558                        // 'swift' => $datos['swift'],
559                        // 'nombre_apoderado' => urlencode($datos['nombre_apoderado']),
560                        // 'nif_apoderado' => $datos['nif_apoderado'],
561                        'fecha' => $issued_date,
562                        'email_contacto' => $client_email,
563                        'region' => $region,
564                    ];
565
566                    $url = $baseUrl.'?'.http_build_query($params);
567
568                    Carbon::setLocale('es');
569
570                    if ($reminder_type < 4) {
571                        $body = str_replace(
572                            ['{{invoice_number}}', '{{client_name}}', '{{issued_date}}', '{{expiration_date}}', '{{amount}}', '{{url}}'],
573                            [
574                                $invoice_number,
575                                $client_name,
576                                Carbon::createFromFormat('Y-m-d', $issued_date)->isoFormat('D [de] MMMM [de] YYYY'),
577                                $expiration_date,
578                                $amount,
579                                $url,
580                            ],
581                            $emailTemplate->html_content
582                        );
583                    } else {
584                        $body = str_replace(
585                            ['{{invoice_number}}', '{{client_name}}', '{{issued_date}}', '{{expiration_date}}', '{{amount}}', '{{table}}'],
586                            [
587                                $invoice_number,
588                                $client_name,
589                                Carbon::createFromFormat('Y-m-d', $issued_date)->isoFormat('D [de] MMMM [de] YYYY'),
590                                $expiration_date,
591                                $amount,
592                                $table,
593                            ],
594                            $emailTemplate->html_content
595                        );
596                    }
597
598                    $email->setSubject($subject);
599                    $email->addContent('text/html', $body);
600                    $email->addTo($toEmail, $client_name);
601
602                    if (is_array($document)) {
603                        foreach ($document as $doc) {
604                            if (! base64_decode((string) $doc['content'], true)) {
605                                throw new \Exception('El documento no es un base64 válido');
606                            }
607
608                            $attachment = new Attachment;
609                            $attachment->setContent($doc['content']);
610                            $attachment->setType('application/pdf');
611                            $attachment->setFilename($doc['filename']);
612                            $attachment->setDisposition('attachment');
613                            $attachment->setContentId('factura_'.uniqid());
614                            $email->addAttachment($attachment);
615
616                        }
617                    }
618
619                    if (! is_array($document)) {
620                        if (! base64_decode((string) $document, true)) {
621                            throw new \Exception('El documento no es un base64 válido');
622                        }
623
624                        $attachment = new Attachment;
625                        $attachment->setContent($document);
626                        $attachment->setType('application/pdf');
627                        $attachment->setFilename("Factura_{$invoice_number}.pdf");
628                        $attachment->setDisposition('attachment');
629                        $attachment->setContentId('factura_'.uniqid());
630                        $email->addAttachment($attachment);
631                    }
632
633                    try {
634                        $response = $sendgrid->send($email);
635                        SendgridLogger::log($email, $response);
636                    } catch (\Throwable $sendException) {
637                        SendgridLogger::logException($email, $sendException);
638                        throw $sendException;
639                    }
640
641                    if ($response->statusCode() == 202) {
642                        $successCount++;
643                        Log::channel('g3w_invoices_sent')->info("Factura $invoice_number enviada exitosamente a: $toEmail");
644                    } else {
645                        $failCount++;
646                        Log::channel('g3w_invoices_not_send')->error("Error enviando a $toEmail".$response->body());
647                    }
648
649                } catch (\Exception $e) {
650                    $failCount++;
651                    Log::channel('g3w_invoices_not_send')->error("Error enviando a $toEmail".$e->getMessage());
652
653                    continue;
654                }
655            }
656
657            if ($successCount > 0) {
658                return [
659                    'success' => true,
660                    'message' => "Enviados: $successCount, Fallidos: $failCount",
661                    'sent_count' => $successCount,
662                    'failed_count' => $failCount,
663                ];
664            } else {
665                return [
666                    'success' => false,
667                    'message' => 'Todos los envíos fallaron',
668                    'sent_count' => $successCount,
669                    'failed_count' => $failCount,
670                ];
671            }
672
673        } catch (\Exception $e) {
674            Log::channel('g3w_invoices_not_send')->error("Error general enviando factura $invoice_number".$e->getMessage());
675
676            return ['success' => false, 'message' => $e->getMessage()];
677        }
678    }
679
680    public function getAllInvoices($region = 'Cataluña')
681    {
682        if ($region === 'All') {
683            $invoices = TblInvoiceReminders::orderBy('id', 'desc')
684                ->paginate(50);
685        } else {
686            $invoices = TblInvoiceReminders::where('region', $region)
687                ->orderBy('id', 'desc')
688                ->paginate(50);
689        }
690
691        return response()->json([
692            'invoices' => $invoices->items(),
693            'pagination' => [
694                'total' => $invoices->total(),
695                'current_page' => $invoices->currentPage(),
696                'per_page' => $invoices->perPage(),
697                'last_page' => $invoices->lastPage(),
698            ],
699        ]);
700    }
701
702    public function getAllInvoicesExceptions()
703    {
704        $invoices = TblInvoicesExceptions::orderBy('id', 'desc')->paginate(50);
705
706        return response()->json([
707            'invoices' => $invoices->items(),
708            'pagination' => [
709                'total' => $invoices->total(),
710                'current_page' => $invoices->currentPage(),
711                'per_page' => $invoices->perPage(),
712                'last_page' => $invoices->lastPage(),
713            ],
714        ]);
715    }
716
717    public function sendCyCInvoices(?string $region): array
718    {
719        try {
720            if (! $region) {
721                throw new Exception('No region provided');
722            }
723
724            $fromDay = 45;
725            $toDay = 35;
726            $today = Carbon::createFromDate(date('Y'), date('m'), date('d'));
727            $todayFormatted = Carbon::now()->format('Y-m-d');
728
729            $documentName = $todayFormatted.'.csv';
730
731            $filePath = storage_path('app/public/uploads/CyC/'.$todayFormatted.'/'.$region.'/'.$documentName);
732
733            if (! file_exists(dirname($filePath))) {
734                mkdir(dirname($filePath), 0777, true);
735            }
736
737            $file = fopen($filePath, 'w');
738
739            fputcsv($file, [
740                'Service',
741                'Language',
742                'Cyc Poliza',
743                'SP Tax Idenfication Number',
744                'SP Country',
745                'BP TaxIdentificationNumber',
746                'SP Corporate Name',
747                'BP Country',
748                'Invoice Number',
749                'Invoice Issue Date',
750                'PD Installment Due Date',
751                'PD Payment Means',
752                'Total Amount',
753                'Taxable Base',
754                'Tax Rate',
755                'Tax Amount',
756            ], ';', escape: '\\');
757
758            while ($fromDay > $toDay) {
759                $date = $today->copy()->subDays($fromDay)->format('Y-m-d');
760                $invoices = $this->request('get', 'factura/vence/'.$date, $region, []);
761                foreach ($invoices['facturas'] as $invoice) {
762                    $invoiceData = $this->request('get', 'factura/'.$invoice['ID'], $region, []);
763                    $invoiceData = $invoiceData['factura'];
764
765                    $dataClient = $this->request('get', 'cliente/'.$invoiceData['cod_cliente'], $region, []);
766                    $invoiceCobrada = $invoiceData['cobrada'];
767                    $invoiceDocument = $invoiceData['documento'];
768                    $invoiceNumber = $invoiceData['n_factura'];
769
770                    if (
771                        ! $invoiceDocument ||
772                        $invoiceCobrada !== 'NO') {
773                        continue;
774                    }
775
776                    if ($region === 'Cataluña' && stripos((string) $invoiceNumber, 'R') === false) {
777                        continue;
778                    }
779
780                    fputcsv($file, [
781                        'grabacionFacturas',
782                        'ESP',
783                        '156547',
784                        'B67795088',
785                        'ESP',
786                        $dataClient['cliente']['cliente_cif'],
787                        $dataClient['cliente']['empresa'],
788                        'ESP',
789                        $invoiceNumber,
790                        $invoiceData['fecha_creacion'],
791                        $invoiceData['vencimientos'][0]['fecha_vencimiento'],
792                        $invoiceData['forma_pago_factura'],
793                        $invoiceData['importe_total_factura'],
794                        $invoiceData['base_imponible_factura'],
795                        $invoiceData['iva_factura'] * 100,
796                        $invoiceData['importe_iva_factura'],
797                    ], ';', escape: '\\');
798
799                }
800                $fromDay--;
801            }
802
803            fclose($file);
804
805            return ['success' => true];
806        } catch (\Exception $e) {
807            Log::channel('g3w_invoices')->error($e->getMessage());
808
809            return ['success' => false, 'error' => $e->getMessage()];
810        }
811    }
812
813    public function setAllMonthAdministratorsInvoices($region): array
814    {
815        if (! $region) {
816            return ['success' => false, 'error' => 'No region provided'];
817        }
818
819        try {
820            $now = Carbon::now();
821            $month = $now->format('m');
822        } catch (\Exception $e) {
823            Log::channel('g3w_invoices')->error('Formato de fecha de mes no válido (esperado YYYY-mm)');
824
825            return ['success' => false, 'error' => 'Formato de fecha de mes no válido (esperado YYYY-mm)'];
826        }
827
828        $invoices = $this->request('get', 'factura/vencemesadministrador/'.$month, $region, []);
829
830        foreach ($invoices['facturas'] as $invoice) {
831            $invoiceData = $this->request('get', 'factura/'.$invoice['ID'], $region, []);
832            $invoiceData = $invoiceData['factura'];
833
834            // Continue if region is Comunidad Valenciana and the invoice starts with M
835            if (
836                $region == 'Comunidad Valenciana'
837                && str_starts_with((string) $invoice['ID'], 'M')
838            ) {
839                continue;
840            }
841
842            $cobrada = $invoiceData['cobrada'];
843            $formaPago = $invoiceData['forma_pago_factura'];
844            // Invoice already paied
845            // Invoice that the payment method is not "transferencia"
846            if (
847                $cobrada !== 'NO'
848                || stripos((string) $formaPago, 'tr') === false
849            ) {
850                continue;
851            }
852
853            $dataClient = [];
854            if ($invoice['cod_administrador']) {
855                $dataClient = $this->request('get', 'cliente/'.$invoice['cod_administrador'], $region, []);
856                $dataClient = $dataClient['cliente'];
857            }
858
859            if ($invoice['cod_cliente']) {
860                $dataClientAdministrado = $this->request('get', 'cliente/'.$invoice['cod_cliente'], $region, []);
861                $dataClientAdministrado = $dataClientAdministrado['cliente'];
862                if ($dataClientAdministrado['tipo_cliente'] === 'Grandes Cuentas') {
863                    continue;
864                }
865            }
866
867            /*if (
868                $dataClient["tipo_cliente"] !== "Administrador"
869                ){
870                continue;
871            }*/
872
873            $codService = $invoiceData['lineas'][0]['cod_servicio'];
874            $dataService = $this->request('get', 'servicio/'.$codService, $region, []);
875            $dataService = $dataService['servicio'];
876
877            TblInvoiceAdministrators::create([
878                'invoice_number' => $invoiceData['n_factura'],
879                'region' => $region,
880                'name' => $dataClient['empresa'],
881                'CIF' => $dataClient['cliente_cif'],
882                'email' => $dataClient['email'],
883                'service_name' => $dataService['nombre_servicio'],
884                'service_addres' => $dataService['direccion'],
885                'send_date' => Carbon::now('Europe/Madrid')->toDateString(),
886                'expiration_date' => Carbon::parse($invoice['fecha_vencimiento'])->toDateString(),
887                'amount' => $invoiceData['importe_total_factura'],
888                'cod_administrador' => $invoice['cod_administrador'],
889                'cod_cliente' => $invoice['cod_cliente'],
890            ]);
891        }
892
893        return [
894            'success' => true,
895            'message' => "Proceso completado para el mes $month en la región $region.",
896        ];
897    }
898
899    /**
900     * @return 'No region provided'[]|'Processing complete'[]|bool[]
901     */
902    public function sendAdministratorsInvoices($region): array
903    {
904        if (! $region) {
905            return ['success' => false, 'error' => 'No region provided'];
906        }
907
908        $month = [
909            1 => 'Enero',
910            2 => 'Febrero',
911            3 => 'Marzo',
912            4 => 'Abril',
913            5 => 'Mayo',
914            6 => 'Junio',
915            7 => 'Julio',
916            8 => 'Agosto',
917            9 => 'Septiembre',
918            10 => 'Octubre',
919            11 => 'Noviembre',
920            12 => 'Diciembre',
921        ];
922
923        $startOfMonth = Carbon::now()->startOfMonth()->toDateString();
924        $endOfMonth = Carbon::now()->endOfMonth()->toDateString();
925
926        $currentMonthInvoices = TblInvoiceAdministrators::where('region', $region)
927            ->where('paid', 0)
928            ->whereBetween('expiration_date', [$startOfMonth, $endOfMonth])
929            ->orderBy('expiration_date', 'asc')
930            ->get();
931
932        $invoicesGroupedByAdministrator = $currentMonthInvoices->groupBy('cod_administrador');
933
934        foreach ($invoicesGroupedByAdministrator as $administratorCode => $administratorInvoices) {
935            $table = "<table style='border-collapse: collapse; width: 100%; font-family: Arial, sans-serif;'>
936            <tr>
937                <th style='border: 1px solid #999; padding: 8px; background-color: #f2f2f2; text-align: left;'>Número Factura</th>
938                <th style='border: 1px solid #999; padding: 8px; background-color: #f2f2f2; text-align: left;'>Nombre cliente de servicio</th>
939                <th style='border: 1px solid #999; padding: 8px; background-color: #f2f2f2; text-align: left;'>Dirección del servicio</th>
940                <th style='border: 1px solid #999; padding: 8px; background-color: #f2f2f2; text-align: left;'>Fecha de emisión</th>
941                <th style='border: 1px solid #999; padding: 8px; background-color: #f2f2f2; text-align: left;'>Fecha de vencimiento</th>
942                <th style='border: 1px solid #999; padding: 8px; background-color: #f2f2f2; text-align: left;'>Importe</th>
943            </tr>";
944
945            $documentsBase64 = [];
946
947            foreach ($administratorInvoices as $invoice) {
948                if ($this->checkClientHasFreshdeskTask($invoice->cod_cliente, $invoice->region)) {
949                    continue;
950                }
951
952                $invoiceNumber = $invoice->invoice_number;
953                $table .= "<tr>
954                    <td class='invoice_number' style='border: 1px solid #999; padding: 8px;'>".$invoiceNumber."</td>
955                    <td class='invoice_service_name' style='border: 1px solid #999; padding: 8px;'>".$invoice->service_name."</td>
956                    <td class='invoice_service_addres' style='border: 1px solid #999; padding: 8px;'>".$invoice->service_addres."</td>
957                    <td class='invoice_send_date' style='border: 1px solid #999; padding: 8px;'>".$invoice->send_date."</td>
958                    <td class='invoice_expiration_date' style='border: 1px solid #999; padding: 8px;'>".$invoice->expiration_date."</td>
959                    <td class='invoice_amount' style='border: 1px solid #999; padding: 8px;'>".$invoice->amount.' €</td>
960                </tr>';
961
962                $invoice = $this->request('get', 'factura/'.$invoiceNumber, $region, []);
963                $invoiceData = $invoice['factura'];
964
965                $documentsBase64[] = [
966                    'content' => $invoiceData['documento'],
967                    'filename' => "Factura_{$invoiceNumber}.pdf",
968                ];
969            }
970
971            $table .= '</table>';
972
973            if (empty($documentsBase64)) {
974                continue;
975            }
976
977            $monthText = $month[Carbon::now()->format('n')];
978
979            $invoiceSendData = $this->sendInvoice(
980                $administratorInvoices->first()->invoice_number,
981                $administratorInvoices->first()->name,
982                $administratorInvoices->first()->email,
983                $administratorInvoices->first()->send_date,
984                $administratorInvoices->first()->expiration_date,
985                $documentsBase64,
986                $administratorInvoices->first()->amount,
987                4,
988                $region,
989                $table,
990                $monthText.' de '.Carbon::now()->format('Y')
991            );
992
993            TblInvoiceReminders::create([
994                'invoice_number' => $administratorInvoices->first()->invoice_number,
995                'client_name' => $administratorInvoices->first()->name,
996                'client_email' => $administratorInvoices->first()->email,
997                'cif' => 'Administradores',
998                'issued_date' => date('Y-m-d'),
999                'expiration_date' => $administratorInvoices->first()->expiration_date,
1000                'amount' => $administratorInvoices->first()->amount,
1001                'collection_date' => null,
1002                'region' => $region,
1003                'reminder_type' => 4,
1004            ]);
1005
1006            TblInvoiceReminders::create([
1007                'invoice_number' => $administratorInvoices->first()->invoice_number,
1008                'client_name' => $administratorInvoices->first()->name,
1009                'client_email' => $administratorInvoices->first()->email,
1010                'cif' => 'Administradores',
1011                'issued_date' => date('Y-m-d'),
1012                'expiration_date' => $administratorInvoices->first()->expiration_date,
1013                'amount' => $administratorInvoices->first()->amount,
1014                'collection_date' => null,
1015                'region' => $region,
1016                'reminder_type' => 4,
1017            ]);
1018
1019        }
1020
1021        return ['success' => true, 'message' => 'Processing complete'];
1022    }
1023
1024    private function checkClientHasFreshdeskTask($client, $region): false
1025    {
1026        try {
1027            $response = Http::withBasicAuth('titan_auto', 'n71Mhh8i7FO_h2fF8e4')
1028                ->post('https://aiwf.ibvgroup.com/webhook/client-lookup', [
1029                    'client_id' => $client,
1030                    'region' => $region,
1031                ]);
1032
1033            if ($response->successful()) {
1034                return $response->json('found', false);
1035            }
1036
1037            return false;
1038
1039        } catch (\Exception $e) {
1040            Log::error('Error on checkClientHasFreshdeskTask: '.$e->getMessage());
1041
1042            return false;
1043        }
1044    }
1045
1046    public function checkClientHasFreshdeskTaskAll(): array|false
1047    {
1048        try {
1049            $clients = TblInvoiceAdministrators::all();
1050            $codClients = [];
1051            foreach ($clients as $client) {
1052                $response = Http::withBasicAuth('titan_auto', 'n71Mhh8i7FO_h2fF8e4')
1053                    ->post('https://aiwf.ibvgroup.com/webhook/client-lookup', [
1054                        'client_id' => $client->cod_cliente,
1055                        'region' => $client->region,
1056                    ]);
1057                $data = $response->json();
1058
1059                if (isset($data['found']) && $data['found'] === true) {
1060                    $codClients[] = "{$client->cod_cliente} en {$client->region}";
1061                }
1062            }
1063
1064            return $codClients;
1065
1066        } catch (\Exception $e) {
1067            Log::error('Error on checkClientHasFreshdeskTask: '.$e->getMessage());
1068
1069            return false;
1070        }
1071    }
1072
1073    public function sendCallCenterInvoices()
1074    {
1075        $spreadsheetId = '1JxCICtqPtPrE8B_0nVJZYwIZ67Nv5gPYr9rW73Pz1_Q';
1076        $sheetName = 'Hoja 1';
1077
1078        try {
1079            $googleSheetsService = $this->getGoogleSheetsService();
1080
1081            $region = [
1082                'G3W Cat' => 'Cataluña',
1083                'G3W Mad' => 'Madrid',
1084                'G3W Val' => 'Comunidad Valenciana',
1085                // "G3W Alm"=>"Andalucia",
1086            ];
1087
1088            $fromEmail = [
1089                'G3W Cat' => 'fire@fire.es',
1090                'G3W Mad' => 'atencion@fire.es',
1091                'G3W Val' => 'facturacionval@fire.es',
1092                // "G3W Alm"=>"documentacion.almeria@fire.es",
1093            ];
1094
1095            // A: Entorno, B: ID Factura, C: Email, D: Enviado, E: Fecha Envío
1096            $range = $sheetName.'!A2:E';
1097            $response = $googleSheetsService->spreadsheets_values->get($spreadsheetId, $range);
1098            $rows = $response->getValues();
1099
1100            if (empty($rows)) {
1101                return;
1102            }
1103
1104            foreach ($rows as $index => $row) {
1105                $currentRowNumber = $index + 2;
1106
1107                $entorno = $row[0] ?? null;
1108                $idFactura = $row[1] ?? null;
1109                $emailToSend = $row[2] ?? null;
1110                $enviado = $row[3] ?? 'NO';
1111
1112                if ($idFactura && $emailToSend && strtoupper($enviado) !== 'SI') {
1113
1114                    try {
1115                        // --- AQUÍ LLAMAS A TU LÓGICA DE ENVÍO ---
1116                        if (
1117                            $region[$entorno] == 'Comunidad Valenciana'
1118                            && str_starts_with((string) $idFactura, 'M')
1119                        ) {
1120                            continue;
1121                        }
1122
1123                        $invoiceData = $this->request('get', 'factura/'.$idFactura, $region[$entorno], []);
1124                        $invoiceData = $invoiceData['factura'];
1125
1126                        $cobrada = $invoiceData['cobrada'];
1127                        $formaPago = $invoiceData['forma_pago_factura'];
1128                        // Invoice already paied
1129                        // Invoice that the payment method is not "transferencia"
1130                        if (
1131                            $cobrada !== 'NO'
1132                            || stripos((string) $formaPago, 'tr') === false
1133                        ) {
1134                            continue;
1135                        }
1136
1137                        $html = '<!doctypehtml><html lang=es><meta charset=UTF-8><meta content="width=device-width,initial-scale=1"name=viewport><title>Recordatorio de Pago - Vencimiento en 1 semana</title><style>body{font-family:Arial,sans-serif;font-size:11;line-height:1.6;max-width:600px;margin:0 auto;padding:20px}@media only screen and (max-width:600px){body{padding:15px}}</style><p>Estimado cliente,</p><p>Le contactamos desde <strong>Grupo Fire</strong>, empresa responsable del mantenimiento de sus extintores, en relación con las <strong>facturas pendientes de pago</strong> que se detallan en los documentos adjuntos.</p><p>Tras revisar nuestra contabilidad, hemos comprobado que, a día de hoy, los importes correspondientes aún no han sido abonados. Por ello, le agradeceríamos que nos indicara si existe alguna incidencia o motivo que haya podido impedir el pago (por ejemplo: cambio de cuenta bancaria, domiciliación no autorizada, disconformidad con la factura, etc.).</p><p>En caso de no existir ninguna incidencia, le agradeceríamos proceder al abono de los importes pendientes a la mayor brevedad. Puede realizar el pago mediante ingreso o transferencia a la cuenta bancaria que figura en las facturas adjuntas.</p><p>Un cordial saludo,<br><img alt=""src="data:image/png;base64,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" style=width:"height: 45px;"></div>';
1138
1139                        $sendgrid = new \SendGrid(config('services.sendgrid.api_key'));
1140
1141                        $successCount = 0;
1142                        $failCount = 0;
1143
1144                        $email = new Mail;
1145
1146                        // $fromEmail = 'recordatorio.factura@fire.es';
1147                        $fromName = 'Recordatorio Facturas Grupo Fire';
1148                        $email->setFrom($fromEmail[$entorno], $fromName);
1149                        $email->setReplyTo($fromEmail[$entorno], 'Recordatorio Facturas Grupo Fire');
1150                        $email->setSubject('Envío de factura '.$idFactura);
1151                        $email->addContent('text/html', $html);
1152                        $email->addTo($emailToSend);
1153
1154                        if (! base64_decode((string) $invoiceData['documento'], true)) {
1155                            throw new \Exception('El documento no es un base64 válido');
1156                        }
1157
1158                        $attachment = new Attachment;
1159                        $attachment->setContent($invoiceData['documento']);
1160                        $attachment->setType('application/pdf');
1161                        $attachment->setFilename($idFactura);
1162                        $attachment->setDisposition('attachment');
1163                        $attachment->setContentId('factura_'.uniqid());
1164                        $email->addAttachment($attachment);
1165
1166                        try {
1167                            $response = $sendgrid->send($email);
1168                            SendgridLogger::log($email, $response);
1169                        } catch (\Throwable $sendException) {
1170                            SendgridLogger::logException($email, $sendException);
1171                            throw $sendException;
1172                        }
1173
1174                        if ($response->statusCode() != 202) {
1175                            throw new Exception('Envio fallido');
1176                        }
1177
1178                        // --- AQUÍ LLAMAS A TU LÓGICA DE ENVÍO ---
1179
1180                        // $updateRange = $sheetName . '!D' . $currentRowNumber . ':E' . $currentRowNumber;
1181                        $updateData = [[$entorno, $idFactura, $emailToSend, 'Si', date('Y-m-d H:i:s')]];
1182
1183                        $this->writeToGoogleSheet(
1184                            $googleSheetsService,
1185                            $spreadsheetId,
1186                            $sheetName,
1187                            $updateData,
1188                            $currentRowNumber
1189                        );
1190
1191                    } catch (\Exception $e) {
1192                        continue;
1193                    }
1194                }
1195            }
1196
1197            return ['success' => true, 'message' => 'Processing complete'];
1198
1199        } catch (\Exception $e) {
1200            Log::error('Error en sendCallCenterInvoices: '.$e->getMessage());
1201
1202            return response()->json(['success' => false, 'message' => $e->getMessage()]);
1203        }
1204    }
1205
1206    // Google sheets
1207    public function addToSheets($codCliente, $invoice, $region, $spreadsheetId = '15Lc9tJnHDOGp33V9RH86mXtIybJBAWWlW4fe7knhDZY')
1208    {
1209        $sheetName = 'Hoja 1';
1210        $googleSheetsService = null;
1211        $nextRow = 1;
1212
1213        try {
1214            $googleSheetsService = $this->getGoogleSheetsService();
1215            $range = $sheetName.'!A:A';
1216            $response = $googleSheetsService->spreadsheets_values->get($spreadsheetId, $range);
1217            $values = $response->getValues();
1218            $nextRow = $values ? count($values) + 1 : 1;
1219
1220        } catch (\Exception $e) {
1221            if (str_contains($e->getMessage(), 'Primera configuración requerida')) {
1222                return [
1223                    'success' => false,
1224                    'message' => $e->getMessage(),
1225                    'requires_auth' => true,
1226                ];
1227            }
1228
1229            return [
1230                'success' => false,
1231                'message' => 'Error de conexión con Google Sheets: '.$e->getMessage(),
1232            ];
1233        }
1234
1235        if ($googleSheetsService && $nextRow === 1) {
1236            $this->writeToGoogleSheet($googleSheetsService, $spreadsheetId, $sheetName, [
1237                ['ID Cliente', 'Número de Factura', 'Fecha de Compromiso de Pago', 'Fecha de Compromiso de Pago + 10 días', 'Region'],
1238            ], 1);
1239            $nextRow = 2;
1240        }
1241
1242        $fechaCompromiso = date('Y-m-d');
1243        $fechaCompromisoMas10 = date('Y-m-d', strtotime('+10 days'));
1244
1245        $data = [
1246            $codCliente,
1247            $invoice,
1248            $fechaCompromiso,
1249            $fechaCompromisoMas10,
1250            $region,
1251        ];
1252
1253        if ($googleSheetsService) {
1254            $this->writeToGoogleSheet($googleSheetsService, $spreadsheetId, $sheetName, [$data], $nextRow);
1255            $nextRow++;
1256        }
1257
1258    }
1259
1260    private function getGoogleSheetsService(): Sheets
1261    {
1262
1263        $redirectUrl = '';
1264        if (config('app.env') === 'production') {
1265            $redirectUrl = 'https://fireservicetitan.com/api/google-sheets-callback';
1266        }
1267
1268        if (config('app.env') === 'staging') {
1269            $redirectUrl = 'https://stg.fireservicetitan.com/api/google-sheets-callback';
1270        }
1271
1272        if (config('app.env') === 'local') {
1273            $redirectUrl = 'http://localhost:8000/api/google-sheets-callback';
1274        }
1275
1276        $client = new Client;
1277        $client->setAuthConfig(storage_path('app/credentials.json'));
1278        $client->addScope(Sheets::SPREADSHEETS);
1279        $client->setAccessType('offline');
1280        $client->setPrompt('select_account consent');
1281        $client->setRedirectUri($redirectUrl);
1282
1283        $tokenPath = storage_path('app/token.json');
1284
1285        if (file_exists($tokenPath)) {
1286            $accessToken = json_decode(file_get_contents($tokenPath), true);
1287            $client->setAccessToken($accessToken);
1288        }
1289
1290        if ($client->isAccessTokenExpired()) {
1291            if ($client->getRefreshToken()) {
1292                $newToken = $client->fetchAccessTokenWithRefreshToken($client->getRefreshToken());
1293                file_put_contents($tokenPath, json_encode($newToken));
1294            } else {
1295                $authUrl = $client->createAuthUrl();
1296                throw new \Exception('Initial setup required. Visit this URL to authorize: '.$authUrl);
1297            }
1298        }
1299
1300        return new Sheets($client);
1301    }
1302
1303    private function writeToGoogleSheet($service, $spreadsheetId, string $sheetName, array $data, int|float $rowNumber)
1304    {
1305        try {
1306            $range = $sheetName.'!A'.$rowNumber.':E'.$rowNumber;
1307
1308            $body = new ValueRange([
1309                'values' => $data,
1310            ]);
1311
1312            $params = [
1313                'valueInputOption' => 'USER_ENTERED',
1314            ];
1315
1316            $result = $service->spreadsheets_values->update(
1317                $spreadsheetId,
1318                $range,
1319                $body,
1320                $params
1321            );
1322
1323            return $result;
1324
1325        } catch (\Exception $e) {
1326            Log::error('❌ Error escribiendo en Google Sheets: '.$e->getMessage());
1327            throw $e;
1328        }
1329    }
1330
1331    public function handleGoogleAuthCallback(Request $request)
1332    {
1333        try {
1334            // Verificar que el código existe
1335            if (! $request->get('code')) {
1336                Log::error('❌ No code parameter found');
1337
1338                return response()->json([
1339                    'success' => false,
1340                    'message' => 'No authorization code provided',
1341                ], 400);
1342            }
1343
1344            $redirectUrl = '';
1345
1346            if (config('app.env') === 'production') {
1347                $redirectUrl = 'https://fireservicetitan.com/api/google-sheets-callback';
1348            }
1349
1350            if (config('app.env') === 'staging') {
1351                $redirectUrl = 'https://stg.fireservicetitan.com/api/google-sheets-callback';
1352            }
1353
1354            if (config('app.env') === 'local') {
1355                $redirectUrl = 'http://localhost:8000/api/google-sheets-callback';
1356            }
1357
1358            $client = new Client;
1359            $client->setAuthConfig(storage_path('app/credentials.json'));
1360            $client->addScope(Sheets::SPREADSHEETS);
1361            $client->setRedirectUri($redirectUrl);
1362
1363            $token = $client->fetchAccessTokenWithAuthCode($request->get('code'));
1364
1365            file_put_contents(storage_path('app/token.json'), json_encode($token));
1366
1367            return response()->json([
1368                'success' => true,
1369                'message' => 'Google Sheets authentication complete! You can now run the invoicing function.',
1370            ]);
1371
1372        } catch (\Exception $e) {
1373            Log::error('Error on Google sheets callback: '.$e->getMessage());
1374            Log::error('Error details: '.$e->getTraceAsString());
1375
1376            return response()->json([
1377                'success' => false,
1378                'message' => 'Error: '.$e->getMessage(),
1379            ], 400);
1380        }
1381    }
1382
1383    private function getVencimientosFormateados(array $dataInvoice): ?string
1384    {
1385        $vencimientos = $dataInvoice['factura']['vencimientos'] ?? [];
1386
1387        if (count($vencimientos) === 0) {
1388            return null;
1389        }
1390
1391        $fechasFormateadas = array_map(fn (array $vencimiento) => Carbon::createFromFormat('Y-m-d', $vencimiento['fecha_vencimiento'])
1392            ->isoFormat('D [de] MMMM [de] YYYY'), $vencimientos);
1393
1394        return implode(' Ã³ ', $fechasFormateadas);
1395    }
1396
1397    public function listCreditDaysOffered($region): array
1398    {
1399        try {
1400
1401            ini_set('max_execution_time', 123456);
1402            $date = now()->subMonth()->toDateString();
1403
1404            $lastMonthClients = $this->request('get', 'cliente/nuevosconunafactura/'.$date, $region, []);
1405
1406            if (! $lastMonthClients) {
1407                throw new \Exception('Error al obtener los clientes o la lista está vacía');
1408            }
1409
1410            $tempData = [];
1411            $monthNames = [
1412                1 => 'Enero', 2 => 'Febrero', 3 => 'Marzo', 4 => 'Abril',
1413                5 => 'Mayo', 6 => 'Junio', 7 => 'Julio', 8 => 'Agosto',
1414                9 => 'Septiembre', 10 => 'Octubre', 11 => 'Noviembre', 12 => 'Diciembre',
1415            ];
1416
1417            foreach ($lastMonthClients as $client) {
1418                $id = $client['ID'];
1419                $clientInvoiceResponse = $this->request('get', 'factura/cliente/'.$id, $region, []);
1420                $invoices = $clientInvoiceResponse['facturas'] ?? [];
1421
1422                // 1. Filtro: Si tiene 2 o más facturas, saltamos (queremos 0 o 1)
1423                if (count($invoices) >= 2) {
1424                    continue;
1425                }
1426
1427                $clientData = $this->request('get', 'cliente/'.$id, $region, []);
1428                $clientData = $clientData['cliente'];
1429
1430                // 2. Inicializamos valores por defecto para clientes SIN factura
1431                $clientValue = 0;
1432                $creditDays = 0;
1433                $invoiceData = null;
1434
1435                // 3. Solo si tiene exactamente 1 factura, buscamos sus datos reales
1436                if (count($invoices) === 1) {
1437                    $invoiceResponse = $this->request('get', 'factura/'.$invoices[0]['numero'], $region, []);
1438                    $invoiceData = $invoiceResponse['factura'];
1439
1440                    $clientValue = $invoiceData['base_imponible_factura'] ?? 0;
1441
1442                    // Calculamos días de crédito solo si existen las fechas
1443                    if (! empty($invoiceData['fecha_creacion']) && ! empty($invoiceData['fecha_emision'])) {
1444                        $creation = Carbon::parse($invoiceData['fecha_creacion']);
1445                        $send = $invoiceData['vencimientos'][0]['fecha_vencimiento']
1446                                    ?? $invoiceData['fecha_emision'];
1447                        $creditDays = $creation->diffInDays($send);
1448                    }
1449                }
1450
1451                $date = ($clientData['fecha_alta'] === '0000-00-00' || empty($clientData['fecha_alta']))
1452                    ? Carbon::now()
1453                    : Carbon::parse($clientData['fecha_alta']);
1454
1455                $year = $date->year;
1456                $monthNum = $date->month;
1457                $nameMes = $monthNames[$monthNum];
1458                $weekNum = 'Semana '.$date->weekOfMonth;
1459
1460                // Estructura de agrupación (agregamos acumuladores)
1461                if (! isset($tempData[$year])) {
1462                    $tempData[$year] = [];
1463                }
1464                if (! isset($tempData[$year][$monthNum])) {
1465                    $tempData[$year][$monthNum] = [
1466                        'month' => $nameMes,
1467                        'value' => 0,
1468                        'credit_days' => 0,
1469                        'weeks' => [],
1470                    ];
1471                }
1472                if (! isset($tempData[$year][$monthNum]['weeks'][$weekNum])) {
1473                    $tempData[$year][$monthNum]['weeks'][$weekNum] = [
1474                        'created_at' => $weekNum,
1475                        'value' => 0,
1476                        'credit_days' => 0,
1477                        'clients' => [],
1478                    ];
1479                }
1480
1481                // Sumamos a los totales del mes/semana
1482                $tempData[$year][$monthNum]['value'] += $clientValue;
1483                $tempData[$year][$monthNum]['weeks'][$weekNum]['value'] += $clientValue;
1484
1485                $tempData[$year][$monthNum]['weeks'][$weekNum]['clients'][] = [
1486                    'cif' => $clientData['cliente_cif'],
1487                    'client_code' => $clientData['cod_cliente'],
1488                    'client_name' => $clientData['empresa'],
1489                    'value' => number_format($clientValue, 2, ',', '.').' €',
1490                    'credit_days' => $creditDays,
1491                    'payment_days' => $clientData['forma_pago'] ?? 'N/A',
1492                    'date' => $date->toDateString(),
1493                    'has_invoice' => count($invoices) === 1, // Flag útil para el frontend
1494                ];
1495            }
1496
1497            // Formateo final
1498            $finalData = [];
1499            foreach ($tempData as $year => $months) {
1500                $yearEntry = ['year' => $year, 'months' => []];
1501                foreach ($months as $m) {
1502                    $m['weeks'] = array_values($m['weeks']);
1503                    $m['value'] = number_format($m['value'], 2, ',', '.').' €';
1504                    $yearEntry['months'][] = $m;
1505                }
1506                $finalData[] = $yearEntry;
1507            }
1508
1509            return ['success' => true, 'data' => $finalData];
1510
1511        } catch (\Exception $e) {
1512            return ['success' => false, 'error' => $e->getMessage()];
1513        }
1514    }
1515}