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