Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
4.81% covered (danger)
4.81%
5 / 104
14.29% covered (danger)
14.29%
1 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
HubspotServiceV2
4.81% covered (danger)
4.81%
5 / 104
14.29% covered (danger)
14.29%
1 / 7
479.31
0.00% covered (danger)
0.00%
0 / 1
 batchUpdate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 sendToHusbpot
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
30
 sendToHubspot
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 sendToHubspotSafe
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 updateHubspotOptOutProperty
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
20
 createHubspotProperties
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 1
72
 formatProperties
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace App\Http\Services;
4
5use App\Http\Models\Auth\User;
6use App\Http\Models\HubspotFailedSync;
7use App\Jobs\ProcessHubspotAsyncJob;
8use HubSpot\Client\Crm\Contacts\ApiException;
9use HubSpot\Client\Crm\Contacts\Model\BatchInputSimplePublicObjectBatchInput;
10use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectBatchInput;
11use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectInput;
12use HubSpot\Discovery\Discovery;
13use HubSpot\Factory;
14use Illuminate\Support\Facades\Config;
15use Illuminate\Support\Facades\Http;
16use Illuminate\Support\Facades\Log;
17
18class HubspotServiceV2
19{
20    /**
21     * To update multiple properties for a single user
22     */
23    public function batchUpdate($hubspotId, array $properties = [])
24    {
25        ProcessHubspotAsyncJob::dispatch($hubspotId, $properties);
26    }
27
28    public function sendToHusbpot(string $hubspotId, array $properties, bool $safe = true): void
29    {
30        $user = User::firstWhere('hubspot_id', $hubspotId);
31
32        if (config('app.env') != 'production') {
33            $email = $user?->email ?? $hubspotId;
34            Log::info("Faking hubspot update for {$email}", $properties);
35
36            return;
37        }
38
39        if (empty($hubspotId) || empty($properties)) {
40            return;
41        }
42
43        $properties = $this->formatProperties($properties);
44
45        $client = Factory::createWithAccessToken(config('hubspotconfig.access_token'));
46
47        $batchInputSimplePublicObjectBatchInput = new BatchInputSimplePublicObjectBatchInput([
48            'inputs' => [
49                new SimplePublicObjectBatchInput([
50                    'properties' => $properties,
51                    'id' => $hubspotId,
52                ]),
53            ],
54        ]);
55
56        if ($safe) {
57            $this->sendToHubspotSafe($client, $batchInputSimplePublicObjectBatchInput, $user, $hubspotId, $properties);
58        } else {
59            $this->sendToHubspot($client, $batchInputSimplePublicObjectBatchInput);
60        }
61    }
62
63    private function sendToHubspot(
64        Discovery $client,
65        BatchInputSimplePublicObjectBatchInput $batchInputSimplePublicObjectBatchInput
66    ) {
67        $client->crm()->contacts()->batchApi()->update($batchInputSimplePublicObjectBatchInput);
68    }
69
70    private function sendToHubspotSafe(
71        Discovery $client,
72        BatchInputSimplePublicObjectBatchInput $batchInputSimplePublicObjectBatchInput,
73        User $user,
74        string $hubspotId,
75        array $properties,
76    ) {
77        try {
78            $this->sendToHubspot($client, $batchInputSimplePublicObjectBatchInput);
79        } catch (ApiException $e) {
80            HubspotFailedSync::create([
81                'user_id' => $user?->id ?? null,
82                'email' => $user?->email ?? null,
83                'hubspot_id' => $hubspotId,
84                'data' => json_encode($properties),
85                'error' => $e->getMessage(),
86            ]);
87        }
88    }
89
90    public function updateHubspotOptOutProperty($email)
91    {
92        if (config('app.env') != 'production') {
93            return;
94        }
95
96        try {
97            $accessToken = Config::get('hubspotconfig.access_token');
98            $url = "https://api.hubapi.com/communication-preferences/v4/statuses/{$email}/unsubscribe-all?verbose=false&channel=EMAIL&optState=OPT_OUT";
99
100            $data = [
101                'optState' => 'OPT_OUT',
102            ];
103
104            $response = Http::withHeaders([
105                'Authorization' => "Bearer {$accessToken}",
106                'Content-Type' => 'application/json',
107            ])->post($url, $data);
108
109            if ($response->successful()) {
110                Log::info("Unsubscribed {$email} from all emails.");
111            } else {
112                Log::error("Failed to unsubscribe {$email}", [
113                    'status' => $response->status(),
114                    'response' => $response->body(),
115                ]);
116            }
117        } catch (\Exception $e) {
118            Log::error('Exception in updateHubspotOptOutProperty: '.$e->getMessage());
119        }
120    }
121
122    public function createHubspotProperties(User $user)
123    {
124        if (config('app.env') != 'production') {
125            $fakeHubspotId = $user->email;
126
127            User::where(['email' => $user->email])->update(['hubspot_id' => $fakeHubspotId]);
128
129            return;
130        }
131
132        $user_contact_hubspot = [
133            'firstname' => $user->first_name,
134            'lastname' => $user->last_name,
135            'email' => $user->email,
136            'email_used_for_login' => $user->email,
137            'flymsg_id' => $user->id,
138            'signup_source' => $user->signup_source,
139        ];
140
141        if ($user->account_creation_date) {
142            // existing user
143            $user_contact_hubspot['account_creation_date'] = $user->account_creation_date;
144            $user_contact_hubspot['flymsg_id'] = $user->id;
145            $user_contact_hubspot['signup_source'] = $user->signup_source;
146        } else {
147            // new user
148            $user_contact_hubspot['account_creation_date'] = date('Y-m-d', strtotime('today midnight'));
149        }
150
151        $date_only = $user->created_at ?? $user->updated_at ?? now();
152        $date_only = explode(' ', $date_only->toDateTime()->format('Y-m-d'));
153        $createdTimestamp = \Carbon\Carbon::parse($date_only[0])->toDateString();
154        $user_contact_hubspot['df_stripe_customer_id'] = $user->stripe_id;
155        $user_contact_hubspot['freemium_subscription_start_date'] = $createdTimestamp;
156        $user_contact_hubspot['freemium_subscription_status_updated_on'] = $createdTimestamp;
157        $user_contact_hubspot['flymsg_freemium_subscription_status'] = 'Active';
158        $user_contact_hubspot['subscription_type'] = empty($user->email_verified_at) ? 'Unverified' : 'Freemium';
159
160        $field_name = 'email';
161        $searchRequest = [
162            'filterGroups' => [[
163                'filters' => [[
164                    'value' => $user->email,
165                    'propertyName' => $field_name,
166                    'operator' => 'EQ',
167                ]],
168            ]],
169        ];
170
171        $client = Factory::createWithAccessToken(config('hubspotconfig.access_token'));
172        $apiResponse = $client->crm()->contacts()->searchApi()->doSearch($searchRequest);
173
174        if (isset($apiResponse['total']) && $apiResponse['total'] > 0) {
175            $response = $apiResponse['results'][0];
176            $hubspot_id = $response['id'];
177            User::where(['email' => $user->email])->update(['hubspot_id' => $hubspot_id]);
178        } else {
179            $user = User::firstWhere('email', $user->email);
180            if (! $user || empty($user->hubspot_id)) {
181                $simplePublicObjectInputForCreate = new SimplePublicObjectInput([
182                    'properties' => $this->formatProperties($user_contact_hubspot),
183                ]);
184
185                $client = Factory::createWithAccessToken(config('hubspotconfig.access_token'));
186                $apiResponse = $client->crm()->contacts()->basicApi()->create($simplePublicObjectInputForCreate);
187                User::where(['email' => $user->email])->update(['hubspot_id' => $apiResponse['id']]);
188            }
189        }
190    }
191
192    /**
193     * Format property values to prevent scientific notation in JSON encoding.
194     *
195     * PHP's json_encode() converts small floats (e.g. 0.00008333) to scientific
196     * notation (e.g. 8.333e-5), which HubSpot cannot parse. This method converts
197     * float values to their decimal string representation.
198     *
199     * @param  array<string, mixed>  $properties
200     * @return array<string, mixed>
201     */
202    private function formatProperties(array $properties): array
203    {
204        return array_map(function ($value) {
205            if (is_float($value)) {
206                return rtrim(rtrim(number_format($value, 8, '.', ''), '0'), '.');
207            }
208
209            return $value;
210        }, $properties);
211    }
212}