Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
80.56% covered (warning)
80.56%
232 / 288
79.07% covered (warning)
79.07%
34 / 43
CRAP
0.00% covered (danger)
0.00%
0 / 1
User
80.56% covered (warning)
80.56%
232 / 288
79.07% covered (warning)
79.07%
34 / 43
96.06
0.00% covered (danger)
0.00%
0 / 1
 getRoleAttribute
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCompanyNameAttribute
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setEmailAttribute
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getEmailAttribute
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 sendWelcomeNotification
68.42% covered (warning)
68.42%
65 / 95
0.00% covered (danger)
0.00%
0 / 1
24.06
 sendPasswordResetNotification
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isPasswordSet
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 linkedSocialAccounts
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fileMetaData
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setting
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 shortcuts
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 flyshares
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 shortcutsSharedWithUser
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 shortcutsSharedWithOthers
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 flycutUsage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 charts
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 role_play_projects
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 flycutsUsed
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 charactersTyped
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 charactersSaved
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 timeSaved
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getChartData
100.00% covered (success)
100.00%
111 / 111
100.00% covered (success)
100.00%
1 / 1
2
 promptUsage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 subscription
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 subscriptionTrials
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 flyMsgAITracking
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getflyMsgAITrackingUsage
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
2
 convertIntegerKeysToWords
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 convertIntegerToWord
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 convertIntegerToOrdinal
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 hubspot_property
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 sales_pro_team_manager
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 pocs
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 stripe
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 taxRates
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 planTaxRates
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 company_group
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 company
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 invitation
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNameAttribute
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getInvitationLinkForAdminPortal
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isPOC
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 newFactory
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace App\Http\Models\Auth;
4
5use App\Http\Helpers\FileManagement\S3\model\FileMetaData;
6use App\Http\Models\Admin\AdminUserInvitation;
7use App\Http\Models\Admin\Company;
8use App\Http\Models\Admin\CompanyGroup;
9use App\Http\Models\Admin\CompanyPOC;
10use App\Http\Models\Auth\BaseUser as Authenticatable;
11use App\Http\Models\Chart;
12use App\Http\Models\ClonedSharedShortcut;
13use App\Http\Models\FlyCutUsage;
14use App\Http\Models\FlyMsgAI\FlyMsgAITracking;
15use App\Http\Models\FlyMsgAI\PromptUsage;
16use App\Http\Models\FlyMsgUserDailyUsage;
17use App\Http\Models\FlyShare;
18use App\Http\Models\HubspotProperties;
19use App\Http\Models\Invitation;
20use App\Http\Models\RolePlayProjects;
21use App\Http\Models\SalesProTeamManager;
22use App\Http\Models\Setting;
23use App\Http\Models\SharesShortcut;
24use App\Http\Models\Shortcut;
25use App\Http\Models\SubscriptionTrials;
26use App\Http\Models\UserReferral;
27use App\Http\Scopes\UserScope;
28use App\Notifications\User\ResetPassword;
29use App\Notifications\User\Welcome;
30use App\Observers\UserObserver;
31use App\Traits\CustomHasRoles;
32use Carbon\Carbon;
33use Database\Factories\Http\Models\Auth\UserFactory;
34use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
35use Illuminate\Contracts\Auth\MustVerifyEmail;
36use Illuminate\Database\Eloquent\Attributes\ObservedBy;
37use Illuminate\Database\Eloquent\Factories\HasFactory;
38use Illuminate\Database\Eloquent\Relations\HasOne;
39use Illuminate\Notifications\Notifiable;
40use Illuminate\Support\Facades\Cache;
41use Laravel\Cashier\Billable;
42use Laravel\Passport\HasApiTokens;
43use MongoDB\BSON\UTCDateTime;
44use MongoDB\Laravel\Eloquent\SoftDeletes;
45use Mpociot\Teamwork\Traits\UserHasTeams;
46use Stripe\Stripe;
47use Stripe\StripeClient;
48
49/**
50 * @property bool $is_beta Whether the user has access to beta features
51 * @property bool $developer_mode Whether the user has developer mode enabled (overrides global remote config)
52 * @property bool $processed_2026_usage Whether the user has processed 2026 usage
53 * @property string $temp_password Temporary password for invited users
54 * @property string $temp_password_expiry Expiry date/time string for the temporary password
55 * @property string|null $company_id The company ID this user belongs to
56 * @property string|null $company_group_id The company group ID this user belongs to
57 * @property string|null $first_name
58 * @property string|null $last_name
59 * @property string|null $avatar
60 * @property-read \App\Http\Models\Admin\Company|null $company
61 */
62#[ObservedBy([UserObserver::class])]
63class User extends Authenticatable implements AuthenticatableContract, MustVerifyEmail
64{
65    use Billable, CustomHasRoles, HasApiTokens, HasFactory, Notifiable, SoftDeletes, UserHasTeams;
66
67    protected $table = 'users';
68
69    protected $fillable = [
70        'first_name',
71        'last_name',
72        'email',
73        'password',
74        'avatar',
75        'signup_source',
76        'hubspot_id',
77        'verification_code',
78        'created_by',
79        'refered_by',
80        'rewardable',
81        'rewards_level',
82        'referrals_count',
83        'referral_key',
84        'emails',
85        'instancy_id',
86        'sales_pro_manager_id',
87        'company_group_id',
88        'deactivated_at',
89        'status',
90        'activation_date',
91        'temp_password',
92        'temp_password_expiry',
93        'company_id',
94        'invited_to_company_by_admin',
95        'invited_to_company',
96        'move_assign',
97        'email_verified_at',
98        'onboardingv2_step',
99        'onboardingv2_presented',
100        'heap_analytics_id',
101        'roleplayCredits',
102        'totalTimeCredits',
103        'projectCredits',
104        'is_beta',
105        'developer_mode',
106    ];
107
108    protected $hidden = [
109        'password',
110        'remember_token',
111    ];
112
113    protected $casts = [
114        'deactivated_at' => 'datetime',
115        'activation_date' => 'datetime',
116    ];
117
118    /**
119     * The relationships that should always be loaded.
120     *
121     * @var array
122     */
123    protected $with = ['company'];
124
125    /**
126     * The accessors to append to the model's array form.
127     *
128     * @var array
129     */
130    protected $appends = ['role', 'company_name'];
131
132    protected function getRoleAttribute(): string
133    {
134        return implode(',', $this->roles());
135    }
136
137    /**
138     * Flatten the related company's display name onto the user payload.
139     *
140     * The role-play frontend (and admin-fe) reads `user.company_name`
141     * directly instead of traversing the nested `company` relation, so
142     * we expose it as a virtual attribute. Returns null when the user
143     * has no company attached, which is the case for individual /
144     * self-serve accounts.
145     */
146    protected function getCompanyNameAttribute(): ?string
147    {
148        return $this->company?->name;
149    }
150
151    /**
152     * Set the email to lower case whenever a User is created.
153     */
154    public function setEmailAttribute(string $value): void
155    {
156        $this->attributes['email'] = strtolower($value);
157    }
158
159    /**
160     * Get the user's email and transform it to lower case.
161     */
162    public function getEmailAttribute(string $value): string
163    {
164        return strtolower($value);
165    }
166
167    public function sendWelcomeNotification()
168    {
169        // Update user rewards and the invitation status
170        $searchArray = ['email' => $this->email, 'status' => 'Pending'];
171
172        $invitation = Invitation::where($searchArray);
173        $invitation_count = $invitation->count();
174
175        if ($invitation_count > 0) {
176            $invitation = $invitation->oldest()->first();
177            if (! empty($invitation)) {
178                $invitation_user_id = $invitation->user_id;
179                if ($invitation_user_id && $invitation_user_id != '') {
180                    $userSearchArray = ['_id' => $invitation_user_id];
181                    $invitation_user = User::where($userSearchArray)->first();
182
183                    $rewards_level = (isset($invitation_user['rewards_level']) && $invitation_user['rewards_level'] != 4) ? $invitation_user['rewards_level'] + 1 : 1;
184                    $referrals_count = (isset($invitation_user['referrals_count'])) ? $invitation_user['referrals_count'] + 1 : 1;
185
186                    $invitation_user->rewardable = 1;
187                    $invitation_user->rewards_level = $rewards_level;
188                    $invitation_user->referrals_count = $referrals_count;
189                    $invitation_user->save();
190
191                    $whereUserReferrals = ['user_id' => $invitation_user_id];
192                    $referralCollection = UserReferral::where($whereUserReferrals);
193                    $referral_count = $referralCollection->count();
194
195                    if ($referral_count > 0) {
196                        $referralData = $referralCollection->first();
197                        $whereExistingUserRefeerals = ['user_id' => $invitation_user_id];
198                        $invitedUserData = [
199                            'first_name' => $invitation->first_name,
200                            'last_name' => $invitation->last_name,
201                            'email' => $invitation->email,
202                            'created_at' => now(),
203                            'updated_at' => now(),
204                        ];
205
206                        $current_flycutData = json_encode($invitedUserData, true);
207                        $existingReferralData = $referralData->toArray();
208                        $existingReferralData['total_referrals'] = $rewards_level;
209                        // $existingReferralData['total_referrals'] = $referralData['total_referrals'] + 1;
210
211                        $existingReferralData[] = $current_flycutData; // Directly set the values in the array
212                        // $existingReferralData = $this->convertIntegerKeysToWords($existingReferralData);
213                        unset($existingReferralData['_id']);
214                        UserReferral::where($whereExistingUserRefeerals)->update($existingReferralData);
215                    } else {
216                        $userReferral = new UserReferral;
217                        $invitedUserData = [
218                            'first_name' => $invitation->first_name,
219                            'last_name' => $invitation->last_name,
220                            'email' => $invitation->email,
221                            'created_at' => now(),
222                            'updated_at' => now(),
223                        ];
224
225                        $referralData[0] = json_encode($invitedUserData, true);
226                        $referralData['total_referrals'] = 1;
227                        $referralData['user_id'] = $invitation_user_id;
228                        $userReferral->fill($referralData);
229                        $userReferral->push();
230                    }
231
232                    $invitationUpdateData = ['status' => 'Completed'];
233                    Invitation::where($searchArray)->update($invitationUpdateData);
234                }
235            }
236        } elseif (isset($this->refered_by) && $this->refered_by != '') {
237            // Get user id for the referral if user directly signup with the link
238            $refUserArray = ['referral_key' => $this->refered_by];
239            $refuser = User::where($refUserArray)->first();
240
241            // Update user refferal counts and referral level in user documents
242            $rewards_level = (isset($refuser['rewards_level']) && $refuser['rewards_level'] != 4) ? $refuser['rewards_level'] + 1 : 1;
243            $referrals_count = (isset($refuser['referrals_count'])) ? $refuser['referrals_count'] + 1 : 1;
244
245            $refuser->rewardable = 1;
246            $refuser->rewards_level = $rewards_level;
247            $refuser->referrals_count = $referrals_count;
248            $refuser->save();
249
250            $whereUserReferrals = ['user_id' => $refuser->id];
251            $referralCollection = UserReferral::where($whereUserReferrals);
252            $referral_count = $referralCollection->count();
253
254            if ($referral_count > 0) {
255                $referralData = $referralCollection->first();
256                $whereExistingUserRefeerals = ['user_id' => $refuser->id];
257                $invitedUserData = [
258                    'first_name' => $this->first_name,
259                    'last_name' => $this->last_name,
260                    'email' => $this->email,
261                    'created_at' => now(),
262                    'updated_at' => now(),
263                ];
264
265                $current_refData = json_encode($invitedUserData, true);
266                $existingReferralData = $referralData->toArray();
267                $existingReferralData['total_referrals'] = $rewards_level;
268                // $existingReferralData['total_referrals'] = $referralData['total_referrals'] + 1;
269                $existingReferralData[] = $current_refData; // Directly set the values in the array
270                // $existingReferralData = $this->convertIntegerKeysToWords($existingReferralData);
271                unset($existingReferralData['_id']);
272                UserReferral::where($whereExistingUserRefeerals)->update($existingReferralData);
273            } else {
274                $userReferral = new UserReferral;
275                $invitedUserData = [
276                    'first_name' => $this->first_name,
277                    'last_name' => $this->last_name,
278                    'email' => $this->email,
279                    'created_at' => now(),
280                    'updated_at' => now(),
281                ];
282
283                $referralData[0] = json_encode($invitedUserData, true);
284                $referralData['total_referrals'] = 1;
285                $referralData['user_id'] = $refuser->id;
286                $userReferral->fill($referralData);
287                $userReferral->push();
288            }
289        }
290
291        if (Cache::has('welcome_email_sent_'.$this->email)) {
292            return;
293        }
294
295        // Set cache for email sent to avoid any duplication. At lease for an hour.
296        Cache::put('welcome_email_sent_'.$this->email, true, now()->addHour());
297        $this->notify(new Welcome($this));
298    }
299
300    public function sendPasswordResetNotification($token)
301    {
302        $this->notify(new ResetPassword($token));
303    }
304
305    public function isPasswordSet()
306    {
307        return ! is_null($this->password);
308    }
309
310    public function linkedSocialAccounts()
311    {
312        return $this->hasMany(LinkedSocialAccount::class);
313    }
314
315    public function fileMetaData()
316    {
317        return $this->hasMany(FileMetaData::class, 'user_id');
318    }
319
320    /**
321     * Get the setting that belongs to the user.
322     */
323    public function setting()
324    {
325        return $this->hasOne(Setting::class);
326    }
327
328    /**
329     * Get the shortcuts that belongs to the user.
330     */
331    public function shortcuts()
332    {
333        return $this->hasMany(Shortcut::class)->withoutGlobalScope(UserScope::class);
334    }
335
336    /**
337     * Get the flyshares that belongs to the user.
338     */
339    public function flyshares()
340    {
341        return $this->hasMany(FlyShare::class);
342    }
343
344    /**
345     * Get the shortcuts that was shared to the user.
346     */
347    public function shortcutsSharedWithUser()
348    {
349        return $this->hasMany(SharesShortcut::class);
350    }
351
352    /**
353     * Get the shortcuts that the user shared with others.
354     */
355    public function shortcutsSharedWithOthers()
356    {
357        return $this->hasMany(ClonedSharedShortcut::class);
358    }
359
360    /**
361     * Get the flycut usage records for the user.
362     */
363    public function flycutUsage()
364    {
365        return $this->hasMany(FlyCutUsage::class);
366    }
367
368    /**
369     * Get the chart records for the user.
370     */
371    public function charts()
372    {
373        return $this->hasMany(Chart::class);
374    }
375
376    /**
377     * Get the role_play_projects records for the user.
378     */
379    public function role_play_projects()
380    {
381        return $this->hasMany(RolePlayProjects::class, 'user_id');
382    }
383
384    /**
385     * Get the total number of flycuts used by the user in a time range.
386     */
387    public function flycutsUsed(string $from): int
388    {
389        $from = Carbon::parse($from);
390
391        return $this->flycutUsage()->whereNull('feature')->where('created_at', '>=', $from)->count();
392    }
393
394    /**
395     * Get the total number of characters typed by the user in a time range.
396     */
397    public function charactersTyped(string $from): int
398    {
399        $from = Carbon::parse($from);
400
401        return $this->flycutUsage()->where('created_at', '>=', $from)->sum('characters_typed');
402    }
403
404    /**
405     * Get the total number of characters saved by the user in a time range.
406     */
407    public function charactersSaved(string $from): int
408    {
409        $from = Carbon::parse($from);
410
411        return $this->flycutUsage()->where('created_at', '>=', $from)->sum('characters_saved');
412    }
413
414    /**
415     * Get the total number of time saved by the user in a time range.
416     */
417    public function timeSaved(string $from): float
418    {
419        $from = Carbon::parse($from);
420
421        return $this->flycutUsage()->where('created_at', '>=', $from)->sum('time_saved');
422    }
423
424    /**
425     * Get chart data for user.
426     */
427    public function getChartData(string $from)
428    {
429        $userId = $this->id;
430        $fromDate = Carbon::parse($from);
431        $startDate = new UTCDateTime($fromDate->getTimestamp() * 1000);
432
433        $result = FlyMsgUserDailyUsage::raw(function ($collection) use ($userId, $startDate) {
434            return $collection->aggregate([
435                [
436                    '$match' => [
437                        'user_id' => $userId,
438                        'created_at' => ['$gte' => $startDate],
439                    ],
440                ],
441                [
442                    '$project' => [
443                        'month' => ['$month' => '$created_at'],
444                        'year' => ['$year' => '$created_at'],
445                        'flycut_count' => ['$ifNull' => ['$flycut_count', 0]],
446                        'sentence_rewrite_count' => ['$ifNull' => ['$sentence_rewrite_count', 0]],
447                        'paragraph_rewrite_count' => ['$ifNull' => ['$paragraph_rewrite_count', 0]],
448                        'fly_grammar_actions' => ['$ifNull' => ['$fly_grammar_actions', 0]],
449                        'fly_grammar_accepted' => ['$ifNull' => ['$fly_grammar_accepted', 0]],
450                        'fly_grammar_autocorrect' => ['$ifNull' => ['$fly_grammar_autocorrect', 0]],
451                        'fly_grammar_autocomplete' => ['$ifNull' => ['$fly_grammar_autocomplete', 0]],
452                        'characters_typed' => ['$ifNull' => ['$characters_typed', 0]],
453                        'time_saved' => ['$ifNull' => ['$time_saved', 0]],
454                        'cost_saved' => ['$ifNull' => ['$cost_savings', 0]],
455                    ],
456                ],
457                [
458                    '$project' => [
459                        'month_year' => [
460                            '$concat' => [
461                                [
462                                    '$arrayElemAt' => [
463                                        ['', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
464                                        '$month',
465                                    ],
466                                ],
467                                ' ',
468                                ['$toString' => '$year'],
469                            ],
470                        ],
471                        'flycut_count' => 1,
472                        'sentence_rewrite_count' => 1,
473                        'paragraph_rewrite_count' => 1,
474                        'fly_grammar_actions' => 1,
475                        'fly_grammar_accepted' => 1,
476                        'fly_grammar_autocorrect' => 1,
477                        'fly_grammar_autocomplete' => 1,
478                        'characters_typed' => 1,
479                        'time_saved' => 1,
480                        'cost_saved' => 1,
481                    ],
482                ],
483                [
484                    '$group' => [
485                        '_id' => '$month_year',
486                        'flycut_count' => ['$sum' => '$flycut_count'],
487                        'sentence_rewrite_count' => ['$sum' => '$sentence_rewrite_count'],
488                        'paragraph_rewrite_count' => ['$sum' => '$paragraph_rewrite_count'],
489                        'fly_grammar_actions' => ['$sum' => '$fly_grammar_actions'],
490                        'fly_grammar_accepted' => ['$sum' => '$fly_grammar_accepted'],
491                        'fly_grammar_autocorrect' => ['$sum' => '$fly_grammar_autocorrect'],
492                        'fly_grammar_autocomplete' => ['$sum' => '$fly_grammar_autocomplete'],
493                        'characters_typed' => ['$sum' => '$characters_typed'],
494                        'time_saved' => ['$sum' => '$time_saved'],
495                        'cost_saved' => ['$sum' => '$cost_saved'],
496                    ],
497                ],
498                [
499                    '$sort' => ['_id' => 1],
500                ],
501                [
502                    '$project' => [
503                        'month_year' => '$_id',
504                        '_id' => 0,
505                        'flycut_count' => 1,
506                        'sentence_rewrite_count' => 1,
507                        'paragraph_rewrite_count' => 1,
508                        'fly_grammar_actions' => 1,
509                        'fly_grammar_accepted' => 1,
510                        'fly_grammar_autocorrect' => 1,
511                        'fly_grammar_autocomplete' => 1,
512                        'characters_typed' => 1,
513                        'time_saved' => 1,
514                        'cost_saved' => 1,
515                    ],
516                ],
517            ]);
518        });
519
520        $months = Carbon::parse($from)->diffInMonths(now());
521        $line_chart_data = [];
522        for ($i = 0; $i <= $months; $i++) {
523            $month = $fromDate->copy()->addMonths($i)->format('M');
524            $month_year = $fromDate->copy()->addMonths($i)->format('M Y');
525
526            $data = $result->firstWhere('month_year', $month_year);
527
528            $line_chart_data[$month_year] = [
529                'flycut_count' => $data->flycut_count ?? 0,
530                'sentence_rewrite_count' => $data->sentence_rewrite_count ?? 0,
531                'paragraph_rewrite_count' => $data->paragraph_rewrite_count ?? 0,
532                'fly_grammar_actions' => $data->fly_grammar_actions ?? 0,
533                'fly_grammar_accepted' => $data->fly_grammar_accepted ?? 0,
534                'fly_grammar_autocorrect' => $data->fly_grammar_autocorrect ?? 0,
535                'fly_grammar_autocomplete' => $data->fly_grammar_autocomplete ?? 0,
536                'characters_typed' => $data->characters_typed ?? 0,
537                'time_saved' => $data->time_saved ?? 0,
538                'cost_saved' => $data->cost_saved ?? 0,
539                'month_year' => $month_year,
540            ];
541        }
542
543        return collect($line_chart_data)->sortBy(function ($item) {
544            return Carbon::parse($item['month_year'])->timestamp;
545        }, SORT_REGULAR, true);
546    }
547
548    public function promptUsage()
549    {
550        return $this->hasOne(PromptUsage::class);
551    }
552
553    /**
554     * Get a subscription instance by name, prioritizing active subscriptions.
555     *
556     * Overrides Cashier's default behavior which simply returns the first
557     * subscription matching the name (ordered by created_at desc). This
558     * ensures an active subscription is returned when one exists, falling
559     * back to the most recent subscription of any status otherwise.
560     *
561     * @param  string  $name
562     * @return \App\Http\Models\Subscription|\Illuminate\Database\Eloquent\Model|null
563     */
564    public function subscription($name = 'default')
565    {
566        $subscriptions = $this->subscriptions->where('name', $name);
567
568        return $subscriptions->firstWhere('stripe_status', 'active')
569            ?? $subscriptions->first();
570    }
571
572    /**
573     * Get the subscription trials records for the user.
574     *
575     * @return \Illuminate\Database\Eloquent\Relations\HasMany
576     */
577    public function subscriptionTrials()
578    {
579        return $this->hasMany(SubscriptionTrials::class);
580    }
581
582    /**
583     * Get the flymsg AI Tracking records for the user.
584     *
585     * @return \Illuminate\Database\Eloquent\Relations\HasMany
586     */
587    public function flyMsgAITracking()
588    {
589        return $this->hasMany(FlyMsgAITracking::class);
590    }
591
592    /**
593     * Get flyMsgAITracking Usage for user.
594     *
595     * @param  string  $feature
596     * @return Illuminate\Database\Eloquent\Collection
597     */
598    public function getflyMsgAITrackingUsage($feature)
599    {
600        $usageData = $this->flyMsgAITracking()
601            ->where('feature', $feature)
602            ->where('created_at', '>=', now()->subMonthsNoOverflow(7))
603            ->get(['created_at']);
604
605        $groupedData = $usageData->groupBy(function ($record) {
606            return Carbon::parse($record->created_at)->format('M Y');
607        })
608            ->map(function ($group) {
609                return $group->count();
610            });
611
612        $months = collect([]);
613        // Anchor at the first of the current month so subMonths() walks
614        // through month labels deterministically. Plain now()->subMonths(N)
615        // can collapse two iterations onto the same label when the day
616        // would overflow into the next month (e.g. Apr 30 - 2 months
617        // resolves to Mar 2, the same label as Apr 30 - 1 month).
618        $cursor = now()->startOfMonth();
619        for ($i = 6; $i >= 0; $i--) {
620            $month = $cursor->copy()->subMonths($i)->format('M Y');
621            $months[$month] = $groupedData[$month] ?? 0;
622        }
623
624        return $months;
625    }
626
627    private function convertIntegerKeysToWords($array)
628    {
629        $result = [];
630
631        foreach ($array as $key => $value) {
632            if (is_int($key)) {
633                $key = $this->convertIntegerToWord($key);
634            }
635
636            $result[$key] = is_array($value) ? $this->convertIntegerKeysToWords($value) : $value;
637        }
638
639        return $result;
640    }
641
642    private function convertIntegerToWord($integer)
643    {
644        $words = [
645            0 => 'first',
646            1 => 'second',
647            2 => 'third',
648            3 => 'fourth',
649            4 => 'fifth',
650            5 => 'sixth',
651            6 => 'seventh',
652            7 => 'eighth',
653            8 => 'ninth',
654            9 => 'tenth',
655        ];
656
657        return isset($words[$integer]) ? $words[$integer] : 'nth';
658    }
659
660    private function convertIntegerToOrdinal($integer)
661    {
662        $ordinal = ['first', 'second', 'third', 'fourth', 'fifth', 'sixth', 'seventh', 'eighth', 'ninth', 'tenth'];
663
664        return isset($ordinal[$integer - 1]) ? $ordinal[$integer - 1] : (string) $integer.'th';
665    }
666
667    public function hubspot_property()
668    {
669        return $this->hasOne(HubspotProperties::class, 'email', 'email');
670    }
671
672    public function sales_pro_team_manager()
673    {
674        return $this->hasOne(SalesProTeamManager::class, 'user_id');
675    }
676
677    /**
678     * Company point of contact persons
679     *
680     * @return \Illuminate\Database\Eloquent\Relations\HasMany
681     */
682    public function pocs()
683    {
684        return $this->hasMany(CompanyPOC::class);
685    }
686
687    /**
688     * Get the Stripe client instance.
689     *
690     * @return \Stripe\StripeClient
691     */
692    public function stripe()
693    {
694        return new StripeClient(config: [
695            'api_key' => config('cashier.secret'),
696        ]);
697    }
698
699    /**
700     * Tax rates a user pays on a subscription
701     *
702     * @return string[] ['tax-rate-id'];
703     */
704    public function taxRates()
705    {
706        return [];
707    }
708
709    /**
710     * Tax rates a user pays on a subscription per plan
711     *
712     * return [
713     *   'plan-id' => ['tax-rate-id'],
714     * ];
715     *
716     * @return []
717     */
718    public function planTaxRates()
719    {
720        return [];
721    }
722
723    public function company_group()
724    {
725        return $this->belongsTo(CompanyGroup::class, 'company_group_id');
726    }
727
728    public function company()
729    {
730        return $this->belongsTo(Company::class, 'company_id');
731    }
732
733    public function invitation(): HasOne
734    {
735        return $this->hasOne(AdminUserInvitation::class, 'email', 'email');
736    }
737
738    public function getNameAttribute()
739    {
740        return $this->first_name.' '.$this->last_name;
741    }
742
743    public function getInvitationLinkForAdminPortal()
744    {
745        return config('romeo.frontend-base-url').'/session/signup?email='.$this->email;
746    }
747
748    public function isPOC()
749    {
750        return $this->pocs()->exists();
751    }
752
753    protected static function newFactory()
754    {
755        return UserFactory::new();
756    }
757}