Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 74
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
FreshdeskWebhookService
0.00% covered (danger)
0.00%
0 / 74
0.00% covered (danger)
0.00%
0 / 10
812
0.00% covered (danger)
0.00%
0 / 1
 processWebhook
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
6
 matchClient
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
12
 matchByCompanyName
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 matchByEmail
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 matchByPhone
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 matchByFiscalId
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 matchByGestionaCode
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 matchByBillingClient
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 hit
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 value
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3namespace App\Services;
4
5use App\Models\Client;
6use App\Models\ClientTicket;
7use Illuminate\Support\Facades\Log;
8
9class FreshdeskWebhookService
10{
11    /**
12     * Process a Freshdesk webhook payload.
13     * Upserts by freshdesk_ticket_id — idempotent.
14     */
15    public function processWebhook(array $payload): ClientTicket
16    {
17        $match = $this->matchClient($payload);
18
19        $data = [
20            'client_id'       => $match['client_id'],
21            'ticket_url'      => $this->value($payload, 'ticket_url'),
22            'subject'         => $this->value($payload, 'subject'),
23            'status'          => $this->value($payload, 'status'),
24            'priority'        => $this->value($payload, 'priority'),
25            'requester_name'  => $this->value($payload, 'requester_name'),
26            'requester_email' => $this->value($payload, 'requester_email'),
27            'requester_phone' => $this->value($payload, 'requester_phone'),
28            'company_name'    => $this->value($payload, 'company_name'),
29            'match_field'     => $match['field'],
30        ];
31
32        $freshdeskTicketId = $payload['freshdesk_ticket_id'] ?? null;
33
34        if ($freshdeskTicketId) {
35            $ticket = ClientTicket::updateOrCreate(
36                ['freshdesk_ticket_id' => (int) $freshdeskTicketId],
37                $data
38            );
39        } else {
40            $ticket = ClientTicket::create($data);
41        }
42
43        return $ticket;
44    }
45
46    /**
47     * Try to match a client from the payload using 6 methods, in priority order.
48     */
49    private function matchClient(array $payload): array
50    {
51        $matchers = [
52            fn() => $this->matchByCompanyName($this->value($payload, 'company_name')),
53            fn() => $this->matchByEmail($this->value($payload, 'requester_email')),
54            fn() => $this->matchByPhone($this->value($payload, 'requester_phone')),
55            fn() => $this->matchByFiscalId($this->value($payload, 'cf_cif')),
56            fn() => $this->matchByGestionaCode($this->value($payload, 'cf_service_client_id')),
57            fn() => $this->matchByBillingClient($this->value($payload, 'cf_billing_client_id')),
58        ];
59
60        foreach ($matchers as $matcher) {
61            $result = $matcher();
62            if ($result !== null) {
63                return $result;
64            }
65        }
66
67        Log::warning('Freshdesk webhook: no client match found', [
68            'freshdesk_ticket_id' => $payload['freshdesk_ticket_id'] ?? null,
69            'company_name' => $this->value($payload, 'company_name'),
70            'requester_email' => $this->value($payload, 'requester_email'),
71        ]);
72
73        return ['client_id' => null, 'field' => null];
74    }
75
76    private function matchByCompanyName(?string $value): ?array
77    {
78        if ($value === null) {
79            return null;
80        }
81
82        $client = Client::whereRaw('LOWER(company_name) = ?', [mb_strtolower($value)])->first();
83
84        return $client ? $this->hit('company_name', $client->id) : null;
85    }
86
87    private function matchByEmail(?string $value): ?array
88    {
89        if ($value === null) {
90            return null;
91        }
92
93        $client = Client::whereRaw('LOWER(contact_email) = ?', [mb_strtolower($value)])->first();
94
95        return $client ? $this->hit('email', $client->id) : null;
96    }
97
98    private function matchByPhone(?string $value): ?array
99    {
100        if ($value === null) {
101            return null;
102        }
103
104        $client = Client::where('contact_phone', $value)->first();
105
106        return $client ? $this->hit('phone', $client->id) : null;
107    }
108
109    private function matchByFiscalId(?string $value): ?array
110    {
111        if ($value === null) {
112            return null;
113        }
114
115        $client = Client::whereRaw('LOWER(fiscal_id) = ?', [mb_strtolower($value)])->first();
116
117        return $client ? $this->hit('fiscal_id', $client->id) : null;
118    }
119
120    private function matchByGestionaCode(?string $value): ?array
121    {
122        if ($value === null) {
123            return null;
124        }
125
126        $client = Client::where('gestiona_client_code', $value)->first();
127
128        return $client ? $this->hit('gestiona_code', $client->id) : null;
129    }
130
131    private function matchByBillingClient(?string $value): ?array
132    {
133        if ($value === null) {
134            return null;
135        }
136
137        $client = Client::where('associated_billing_client', $value)->first();
138
139        return $client ? $this->hit('billing_client', $client->id) : null;
140    }
141
142    private function hit(string $field, int $clientId): array
143    {
144        return [
145            'client_id' => $clientId,
146            'field'     => $field,
147        ];
148    }
149
150    /**
151     * Extract a value from the payload, returning null for empty strings
152     * and unfilled placeholders like {{ticket.subject}}.
153     */
154    private function value(array $payload, string $key): ?string
155    {
156        $val = $payload[$key] ?? null;
157
158        if ($val === null || $val === '') {
159            return null;
160        }
161
162        $val = (string) $val;
163
164        if (preg_match('/^\{\{.+\}\}$/', trim($val))) {
165            return null;
166        }
167
168        return $val;
169    }
170}