Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
6.45% covered (danger)
6.45%
10 / 155
20.00% covered (danger)
20.00%
1 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
IfexController
6.45% covered (danger)
6.45%
10 / 155
20.00% covered (danger)
20.00%
1 / 5
870.32
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 request
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
132
 syncQuotations
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
30
 createQuotation
0.00% covered (danger)
0.00%
0 / 57
0.00% covered (danger)
0.00%
0 / 1
90
 updateQuotation
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2
3namespace App\Http\Controllers;
4
5use App\Exceptions\AppException;
6use App\Models\TblIfexLastUpdate;
7use App\Models\TblQuotations;
8use App\Services\PresupuestosService;
9use Carbon\Carbon;
10use Illuminate\Support\Facades\Log;
11
12class IfexController extends Controller
13{
14    protected $statusNormalize;
15
16    public function __construct(protected PresupuestosService $presupuestosService)
17    {
18        $this->statusNormalize = [
19            'Oportunidad' => 1, // Oportunidad => Nuevo
20            'Pendiente' => 11, // Pendiente => Listo para enviar
21            'Pendiente Cliente' => 2, // Pendiente Cliente => Enviado
22            'Aceptado' => 3, // Aceptado => Aceptado
23            'En ejecución' => 12, // En ejecución => En proceso
24            'Rechazado' => 4, // Rechazado => Rechazado
25            'Anulado' => 5, // Anulado => Anulado
26            'Finalizado' => 3, // Finalizado => Aceptado
27        ];
28
29    }
30
31    protected function request($method, $endpoint, array $data = [])
32    {
33        try {
34            // $this->getCredentials($region);
35
36            /*if (!$this->apiUrl) {
37                throw new \Exception('API URL is not defined.');
38            }
39
40            if (!$this->accessToken) {
41                $this->auth($region);
42            }*/
43            $apiUrl = env('IFEX_API_URL', '');
44
45            $url = "{$apiUrl}{$endpoint}";
46            $curl = curl_init();
47
48            curl_setopt_array($curl, [
49                CURLOPT_URL => $url,
50                CURLOPT_RETURNTRANSFER => true,
51                CURLOPT_CUSTOMREQUEST => strtoupper((string) $method),
52                CURLOPT_HTTPHEADER => [
53                    'x-api-key: '.env('IFEX_API_KEY', ''),
54                ],
55            ]);
56
57            if (in_array(strtoupper((string) $method), ['POST', 'PUT', 'PATCH']) && ! empty($data)) {
58                if (! is_array($data)) {
59                    throw new \Exception('The $data parameter must be an array.');
60                }
61                curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
62            }
63
64            $response = curl_exec($curl);
65
66            if (curl_errno($curl)) {
67                throw new \Exception('cURL error: '.curl_error($curl));
68            }
69
70            $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
71            curl_close($curl);
72
73            /*if ($httpCode === 401) {
74                $this->auth($region);
75                return $this->request($method, $endpoint, $region, $data);
76            }*/
77
78            if ($httpCode < 200 || $httpCode >= 300) {
79                Log::channel('ifex')->error('API Response Error: '.$response);
80
81                $errorResponse = json_decode($response, true);
82                $errorMessage = is_array($errorResponse) && isset($errorResponse['message'])
83                    ? $errorResponse['message']
84                    : 'Request error: '.$response;
85
86                throw new \Exception($errorMessage);
87            }
88
89            $responseData = json_decode($response, true);
90
91            if (json_last_error() !== JSON_ERROR_NONE) {
92                throw new \Exception('Invalid JSON response: '.$response);
93            }
94
95            return $responseData;
96        } catch (\Exception $e) {
97            Log::channel('g3w')->error('Error in request: '.$e->getMessage());
98            throw $e;
99        }
100    }
101
102    public function syncQuotations()
103    {
104        try {
105            $today = Carbon::now()->format('Y-m-d');
106            $requestDate = "presupuestos/creados-editados?fechainicio={$today}&fechafin={$today}";
107
108            $quotationsToSync = json_decode(json_encode($this->request('GET', $requestDate)));
109
110            foreach ($quotationsToSync as $quotation) {
111                if (TblQuotations::where('internal_quote_id', $quotation->codigo)->exists()) {
112                    $this->updateQuotation($quotation);
113                } else {
114                    $this->createQuotation($quotation);
115                }
116
117            }
118
119            TblIfexLastUpdate::where('id', 1)->first()->update(
120                [
121                    'last_date' => Carbon::now()->format('Y-m-d H:i:s'),
122                ]
123            );
124
125            return ['success' => true, 'quotations' => $quotationsToSync];
126        } catch (\Exception $e) {
127            report(AppException::fromException($e, 'SYNC_QUOTATIONS_IFEX_EXCEPTION'));
128            Log::channel('ifex')->error('Error updating quotations: '.$e->getMessage(), [
129                'exception' => $e,
130            ]);
131
132            return response()->json([
133                'success' => false,
134                'message' => 'Failed to update ifex quotations',
135                'error' => $e->getMessage(),
136                'error_code' => $e->exceptionCode ?: 500,
137            ], 500);
138        }
139    }
140
141    public function createQuotation($quotation): void
142    {
143        if (! $quotation || empty($quotation->codigo)) {
144            Log::channel('ifex')->warning(
145                'IfexController::createQuotation skipped — missing quotation or empty codigo'
146            );
147
148            return;
149        }
150
151        // instalaciones 6 25O
152        // correctivos 3 25C
153        // mantenimiento 1 25P
154        // Seed from an existing row's budget_type_id if one is already in
155        // tbl_quotations under this codigo — defensive against TOCTOU /
156        // direct callers that bypass syncQuotations' exists() pre-check.
157        // Falls back to null when no row exists (normal create path).
158        $type = TblQuotations::where('internal_quote_id', $quotation->codigo)->value('budget_type_id');
159        if (str_contains((string) $quotation->codigo, '25O')) {
160            $type = 6;
161        }
162
163        if (str_contains((string) $quotation->codigo, '25C')) {
164            $type = 3;
165        }
166
167        if (str_contains((string) $quotation->codigo, '25P')) {
168            $type = 1;
169        }
170        if (str_contains((string) $quotation->codigo, 'G25')) {
171            return;
172        }
173
174        $generateNumber = $this->presupuestosService->generateQuoteId(30);
175
176        if (! is_array($generateNumber) || empty($generateNumber['id'])) {
177            Log::channel('ifex')->error(
178                'IfexController::createQuotation aborted — generateQuoteId returned unexpected shape for codigo '.$quotation->codigo
179            );
180
181            return;
182        }
183
184        $g3wNewId = $generateNumber['id'];
185        $newQuoteId = $generateNumber['number'] ?? null;
186
187        $ifexArray = [
188            'internal_quote_id' => $quotation->codigo,
189            'quote_id' => $newQuoteId,
190            'company_id' => 30,
191            'customer_type_id' => 2,
192            'segment_id' => null,
193            'budget_type_id' => $type,
194            'budget_status_id' => $this->statusNormalize[$quotation->estado] ?? null,
195            'source_id' => 55,
196            'client' => $quotation->clienteNombre ?? null,
197            'phone_number' => $quotation->clienteTelefono ?? null,
198            'email' => $quotation->clienteMail ?? null,
199            'issue_date' => $quotation->fecha ?? null,
200            'request_date' => $quotation->fechaAlta ?? null,
201            'acceptance_date' => $quotation->fechaAceptacion ?? null,
202            'amount' => $quotation->baseImponible ?? null,
203            'last_follow_up_date' => null,
204            'commercial' => 'Ifex',
205            'created_at' => $quotation->fecha ?? null,
206            'created_by' => 'Ifex',
207            'has_attachment' => 0,
208            'cost_of_labor' => 0,
209            'total_cost_of_job' => 0,
210            'invoice_margin' => 0,
211            'margin_for_the_company' => 0,
212            'revenue_per_date_per_worked' => 0,
213            'gross_margin' => 100,
214            'labor_percentage' => 0,
215            'sync_import' => 1,
216            'box_work_g3w' => null,
217            'for_add' => 0,
218            'for_approval' => 0,
219            'reason_for_not_following_up_id' => 2,
220        ];
221
222        TblQuotations::where('id', $g3wNewId)->update($ifexArray);
223    }
224
225    public function updateQuotation($quotation): void
226    {
227        $fstQuotation = TblQuotations::where('internal_quote_id', $quotation->codigo)->first();
228
229        // Caller `syncQuotations` already pre-checks `exists()` before
230        // dispatching here, but TOCTOU and direct callers can still land
231        // with no matching row. Bail out rather than NPE on the property
232        // reads / final `update()` below.
233        if (! $fstQuotation) {
234            Log::channel('ifex')->warning(
235                'IfexController::updateQuotation skipped — no tbl_quotations row for codigo '.$quotation->codigo
236            );
237
238            return;
239        }
240
241        // instalaciones 6 25O
242        // correctivos 3 25C
243        // mantenimiento 1 25P
244        // Seed with the row's current budget_type_id so an unmapped codigo
245        // (one that doesn't contain 25O/25C/25P) preserves the existing
246        // value instead of nulling it on every sync run.
247        $type = $fstQuotation->budget_type_id;
248        if (str_contains((string) $quotation->codigo, '25O')) {
249            $type = 6;
250        }
251
252        if (str_contains((string) $quotation->codigo, '25C')) {
253            $type = 3;
254        }
255
256        if (str_contains((string) $quotation->codigo, '25P')) {
257            $type = 1;
258        }
259        if (str_contains((string) $quotation->codigo, 'G25')) {
260            return;
261        }
262
263        $ifexArray = [
264            'internal_quote_id' => $quotation->codigo,
265            'budget_type_id' => $type,
266            'budget_status_id' => $this->statusNormalize[$quotation->estado],
267            'client' => $quotation->clienteNombre ?? null,
268            'phone_number' => $quotation->clienteTelefono ?? null,
269            'email' => $quotation->clienteMail ?? null,
270            'issue_date' => $quotation->fecha ?? null,
271            'request_date' => $quotation->fechaAlta ?? null,
272            'acceptance_date' => $quotation->fechaAceptacion ?? null,
273            'amount' => $quotation->baseImponible ?? null,
274            'updated_at' => Carbon::now()->format('Y-m-d H:i:s'),
275            'updated_by' => 'Ifex',
276        ];
277
278        $fstQuotation->update($ifexArray);
279
280    }
281}