Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
18.18% covered (danger)
18.18%
68 / 374
14.29% covered (danger)
14.29%
4 / 28
CRAP
0.00% covered (danger)
0.00%
0 / 1
SubscriptionTrait
18.18% covered (danger)
18.18%
68 / 374
14.29% covered (danger)
14.29%
4 / 28
9386.27
0.00% covered (danger)
0.00%
0 / 1
 getPlanFeatureService
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getFeatureByLegacyKey
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getFeatureByNewKey
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getCurrentPlan
85.71% covered (warning)
85.71%
18 / 21
0.00% covered (danger)
0.00%
0 / 1
8.19
 getCurrentAddOns
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 getGrammarQuota
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
2.00
 getQuotaUsed
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getFlyAIQuota
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 getFlyCutsQuota
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
1.00
 getPromptQuota
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
1.00
 getCurrentSubscription
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
72
 extractStylesFromHtml
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 hasTag
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 hasHyperlink
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 checkIfStyleExists
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 countStyles
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getMediaStorage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 checkStylesTagsPermissions
0.00% covered (danger)
0.00%
0 / 118
0.00% covered (danger)
0.00%
0 / 1
1640
 checkIfGiphyExists
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
20
 getDomainName
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 checkCharacterCount
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
56
 checkFlyCutsCount
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
56
 restrictAdvacedSearch
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 checkCategoriesCount
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
132
 checkSubCategoriesCount
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 checkFlyPlates
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
132
 checkShortcutVersionRollBackCount
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
30
 retrieveCouponObject
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace App\Traits;
4
5use App\Helpers\Constants;
6use App\Http\Models\Auth\User;
7use App\Http\Models\FlyGrammarActions;
8use App\Http\Models\FlyMsgUserDailyUsage;
9use App\Http\Models\Plans;
10use App\Http\Models\Prompts\CustomPrompts;
11use App\Http\Models\Shortcut;
12use App\Http\Models\ShortcutCategory;
13use App\Http\Models\ShortcutSubCategoryLv1;
14use App\Http\Models\TemplateCategory;
15use App\Http\Models\UserAddOns;
16use App\Http\Services\PlanFeatureService;
17use App\Traits\AccountCenter\Reporting\FlyMsgAITrackingTrait;
18use Carbon\Carbon;
19use DOMDocument;
20use Illuminate\Http\Request;
21use Illuminate\Support\Facades\Log;
22use Illuminate\Support\Str;
23use MongoDB\BSON\UTCDateTime;
24use Stripe\StripeClient;
25
26/**
27 * Trait for subscription-related functionality.
28 *
29 * Provides methods for checking plan features, quotas, and permissions.
30 * Uses the PlanFeatureService for unified feature access with backward
31 * compatibility for both new and legacy plan structures.
32 */
33trait SubscriptionTrait
34{
35    use FlyMsgAITrackingTrait;
36
37    /**
38     * Cached instance of PlanFeatureService.
39     */
40    private ?PlanFeatureService $planFeatureServiceInstance = null;
41
42    /**
43     * Get the PlanFeatureService instance.
44     */
45    protected function getPlanFeatureService(): PlanFeatureService
46    {
47        if ($this->planFeatureServiceInstance === null) {
48            $this->planFeatureServiceInstance = app(PlanFeatureService::class);
49        }
50
51        return $this->planFeatureServiceInstance;
52    }
53
54    /**
55     * Get a feature value from the current plan using legacy key.
56     *
57     * This method provides backward compatibility by accepting legacy feature keys
58     * and resolving them through the PlanFeatureService.
59     *
60     * @param  Plans  $plan  The plan to get the feature from
61     * @param  string  $legacyKey  The legacy feature key (e.g., 'Bold', 'Categories')
62     * @param  mixed  $default  Default value if feature is not found
63     * @return mixed The feature value
64     */
65    protected function getFeatureByLegacyKey(Plans $plan, string $legacyKey, mixed $default = null): mixed
66    {
67        $value = $this->getPlanFeatureService()->getFeatureValueByLegacyKey($plan, $legacyKey);
68
69        return $value ?? $default;
70    }
71
72    /**
73     * Get a feature value from the current plan using new key.
74     *
75     * @param  Plans  $plan  The plan to get the feature from
76     * @param  string  $key  The new feature key (e.g., 'bold', 'categories_count')
77     * @param  mixed  $default  Default value if feature is not found
78     * @return mixed The feature value
79     */
80    protected function getFeatureByNewKey(Plans $plan, string $key, mixed $default = null): mixed
81    {
82        $value = $this->getPlanFeatureService()->getFeatureValue($plan, $key);
83
84        return $value ?? $default;
85    }
86
87    /**
88     * Get the current plan for a user.
89     *
90     * @param  User  $user
91     * @return Plans
92     */
93    public function getCurrentPlan($user)
94    {
95        $currentSubscription = $user->subscription('main');
96
97        if (filled($currentSubscription) && $currentSubscription->valid() && filled($currentSubscription->plan)) {
98            // Team users a not managed on stripe so once subscription
99            // is cancelled in DB then thats it, no grace periods, no nothing.
100            $is_team_user = $currentSubscription->plan->identifier == Plans::ProPlanTeamsSMB || $currentSubscription->plan->identifier == Plans::ProPlanTeamsENT;
101            if (($is_team_user && ! $currentSubscription->proTeamEnded()) || ! $is_team_user) {
102                return $currentSubscription->plan;
103            }
104        }
105
106        return Plans::select(
107            'title',
108            'identifier',
109            'currency',
110            'interval',
111            'unit_amount',
112            'user_persona_available',
113            'user_custom_prompts',
114            'has_fly_learning',
115            'regenerate_count',
116            'flygrammar_actions',
117            'flycut_deployment',
118            'prompts_per_day',
119            'can_disable_flygrammar'
120        )
121            ->firstWhere('identifier', Plans::FREEMIUM_IDENTIFIER);
122    }
123
124    /**
125     * Get current add-ons for a user.
126     *
127     * @return \Illuminate\Database\Eloquent\Collection
128     */
129    public function getCurrentAddOns(string $userId)
130    {
131        $userAddOns = UserAddOns::where('user_id', $userId)
132            ->where('status', 'active')
133            ->orderBy('created_at', 'desc')
134            ->get()
135            ->load('addOn');
136
137        $userAddOns = $userAddOns->sortBy(function ($userAddOn) {
138            return $userAddOn->addOn->priority ?? 9999;
139        });
140
141        return $userAddOns;
142    }
143
144    /**
145     * Get grammar quota for a user.
146     *
147     * @return array{total: int, used: int, remaining: int}
148     */
149    public function getGrammarQuota(User $user)
150    {
151        $plan = $this->getCurrentPlan($user);
152
153        $today = now();
154
155        $actionsPerformedToday = FlyGrammarActions::where('user_id', $user->id)->where('created_at', '>=', new UTCDateTime(strtotime($today->startOfDay()->toDateTimeString()) * 1000))->where('created_at', '<=', new UTCDateTime(strtotime($today->endOfDay()->toDateTimeString()) * 1000))->count();
156
157        // Use PlanFeatureService for database-driven quota with fallback
158        $total = $this->getFeatureByNewKey($plan, 'flygrammar_actions')
159            ?? $plan->flygrammar_actions
160            ?? 0;
161
162        $remaining = $total - $actionsPerformedToday;
163
164        return [
165            'total' => $total,
166            'used' => $actionsPerformedToday,
167            'remaining' => $total === -1 ? -1 : max($remaining, 0),
168        ];
169    }
170
171    /**
172     * Get quota used for a user.
173     *
174     * @param  string  $userId
175     * @return int
176     */
177    public function getQuotaUsed($userId)
178    {
179        $tracking = $this->getTrackingUserId($userId);
180
181        return $tracking ? $tracking->count() : 0;
182    }
183
184    /**
185     * Get FlyAI quota for a user.
186     *
187     * @param  User  $user
188     * @return array{used: int, total: int, remaining: int, seconds_remaining_until_next_prompt_refill: int}
189     */
190    public function getFlyAIQuota($user)
191    {
192        $currentSubscriptionPlan = $this->getCurrentPlan($user);
193
194        // Use database-driven quota with fallback to hardcoded values
195        $total = Constants::getPromptQuotaForPlan($currentSubscriptionPlan->identifier);
196
197        $used = $this->getQuotaUsed($user->id);
198
199        return [
200            'used' => $used,
201            'total' => $total,
202            'remaining' => max($total - $used, 0),
203            'seconds_remaining_until_next_prompt_refill' => now()->diffInSeconds(now()->startOfDay()->addDay()),
204        ];
205    }
206
207    /**
208     * Get FlyCuts quota for a user.
209     *
210     * @param  User  $user
211     * @return array{used: int, total: int, remaining: int, seconds_remaining_until_next_prompt_refill: int}
212     */
213    public function getFlyCutsQuota($user)
214    {
215        $currentSubscriptionPlan = $this->getCurrentPlan($user);
216
217        // Use PlanFeatureService for database-driven quota with fallback
218        $total = $this->getFeatureByNewKey($currentSubscriptionPlan, 'flycut_deployment')
219            ?? $currentSubscriptionPlan['flycut_deployment']
220            ?? 0;
221
222        $startDay = Carbon::now()->startOfDay();
223
224        $used = FlyMsgUserDailyUsage::where('user_id', $user->id)
225            ->where('created_at', '>=', new UTCDateTime($startDay->getTimestamp() * 1000))
226            ->first()
227            ->flycut_count ?? 0;
228
229        return [
230            'used' => $used,
231            'total' => $total,
232            'remaining' => max($total - $used, 0),
233            'seconds_remaining_until_next_prompt_refill' => now()->diffInSeconds(now()->startOfDay()->addDay()),
234        ];
235    }
236
237    /**
238     * Get prompt quota for a user.
239     *
240     * @return array{used: int, total: int, remaining: int}
241     */
242    public function getPromptQuota(User $user)
243    {
244        $currentSubscriptionPlan = $this->getCurrentPlan($user);
245        $promptsQuotaUsed = CustomPrompts::where('user_id', $user->id)->count();
246
247        // Use PlanFeatureService for database-driven quota with fallback
248        $total = $this->getFeatureByNewKey($currentSubscriptionPlan, 'user_custom_prompts')
249            ?? $currentSubscriptionPlan->user_custom_prompts
250            ?? 0;
251
252        return [
253            'used' => $promptsQuotaUsed,
254            'total' => $total,
255            'remaining' => max($total - $promptsQuotaUsed, 0),
256        ];
257    }
258
259    /**
260     * Get the current subscription for a user.
261     *
262     * @param  User  $user
263     * @return \App\Http\Models\Subscription|null
264     */
265    public function getCurrentSubscription($user)
266    {
267        $currentSubscription = $user->subscription('main');
268
269        if (filled($currentSubscription) && $currentSubscription->valid() && filled($currentSubscription->plan)) {
270            // Team users a not managed on stripe so once subscription
271            // is cancelled in DB then thats it, no grace periods, no nothing.
272            $is_team_user = $currentSubscription->plan->identifier == Plans::ProPlanTeamsSMB || $currentSubscription->plan->identifier == Plans::ProPlanTeamsENT;
273            if ($is_team_user && ! $currentSubscription->proTeamEnded()) {
274                return $currentSubscription;
275            } elseif (! $is_team_user) {
276                return $currentSubscription;
277            }
278        }
279
280        return null;
281    }
282
283    /**
284     * Extract styles from HTML content.
285     *
286     * @param  string  $html
287     * @return array<string, array<string>>
288     */
289    protected function extractStylesFromHtml($html)
290    {
291        $re = '/style="(.+?)"/m';
292
293        preg_match_all($re, $html, $matches, PREG_SET_ORDER, 0);
294
295        $styles = [];
296
297        collect($matches)->map(function ($match) use (&$styles) {
298            $arr = explode(';', $match[1]);
299
300            foreach ($arr as $prop) {
301                $new_arr_explode = explode(':', $prop);
302                $attr = trim($new_arr_explode[0]);
303
304                if (! array_key_exists($attr, $styles) && $attr != '') {
305                    $styles[$attr] = [];
306                }
307
308                $styles[$attr][] = @trim($new_arr_explode[1]);
309            }
310        });
311
312        return $styles;
313    }
314
315    /**
316     * Check if HTML contains a specific tag pattern.
317     *
318     * @param  string  $re  Regex pattern
319     * @param  string  $html  HTML content
320     * @return bool
321     */
322    protected function hasTag($re, $html)
323    {
324        preg_match_all($re, $html, $matches, PREG_SET_ORDER, 0);
325
326        return ! empty($matches);
327    }
328
329    /**
330     * Check if HTML contains a hyperlink.
331     *
332     * @param  string  $re  Regex pattern (unused, kept for backward compatibility)
333     * @param  string  $html  HTML content
334     * @return bool
335     */
336    protected function hasHyperlink($re, $html)
337    {
338        $re = '/<a(.+?)<\/a>/m';
339
340        preg_match_all($re, $html, $matches, PREG_SET_ORDER, 0);
341
342        return ! empty($matches);
343    }
344
345    /**
346     * Check if a specific style exists in the extracted styles.
347     *
348     * @param  string  $key  Style property key
349     * @param  string  $value  Style property value
350     * @param  array<string, array<string>>  $styles  Extracted styles
351     * @return bool
352     */
353    protected function checkIfStyleExists($key, $value, $styles)
354    {
355        return in_array($value, $styles[$key] ?? []) && (count(array_unique($styles[$key] ?? [])) == 1);
356    }
357
358    /**
359     * Count unique style values for a key.
360     *
361     * @param  string  $key  Style property key
362     * @param  array<string, array<string>>  $styles  Extracted styles
363     * @return int
364     */
365    protected function countStyles($key, $styles)
366    {
367        $targeted_styles = $styles[$key] ?? [];
368
369        return count(array_unique($targeted_styles));
370    }
371
372    /**
373     * Get total media storage used by a user.
374     *
375     * @return int
376     */
377    protected function getMediaStorage(User $user)
378    {
379        return $user->fileMetaData()->sum('size');
380    }
381
382    /**
383     * Check if the user has permission to use various text formatting styles.
384     *
385     * Validates HTML content against the user's plan features.
386     *
387     * @param  string  $html
388     * @return array{error: bool, message: string}|true
389     */
390    protected function checkStylesTagsPermissions(Request $request, $html)
391    {
392        $html = str_replace("\n", '', str_replace(PHP_EOL, '', $html));
393
394        $current_subscription = $this->getCurrentPlan($request->user());
395
396        /** Check for Bold Text */
397        $bold = $this->getFeatureByNewKey($current_subscription, 'bold', false);
398        $has_bold_text_in_html = $this->hasTag('/<strong>(.+?)<\/strong>/m', $html);
399
400        if (! $bold && ($has_bold_text_in_html)) {
401            return [
402                'error' => true,
403                'message' => 'You are not authorized to add bold text',
404            ];
405        }
406
407        /** Check for Italic Text */
408        $italic = $this->getFeatureByNewKey($current_subscription, 'italic', false);
409        $has_italic_text_in_html = $this->hasTag('/<em>(.+?)<\/em>/m', $html);
410
411        if (! $italic && ($has_italic_text_in_html)) {
412            return [
413                'error' => true,
414                'message' => 'You are not authorized to add italic text',
415            ];
416        }
417
418        $html_styles = $this->extractStylesFromHtml($html);
419
420        /** Check for underline */
421        $under_line = $this->getFeatureByNewKey($current_subscription, 'underline', false);
422        $under_line_exists = $this->checkIfStyleExists('text-decoration', 'underline', $html_styles);
423
424        if ($under_line_exists && ! $under_line) {
425            return [
426                'error' => true,
427                'message' => 'You are not authorized to add underline text',
428            ];
429        }
430
431        /** Strikethrough */
432        $strike_through = $this->getFeatureByNewKey($current_subscription, 'strikethrough', false);
433        $strike_through_exists = $this->checkIfStyleExists('text-decoration', 'line-through', $html_styles);
434
435        if ($strike_through_exists && ! $strike_through) {
436            return [
437                'error' => true,
438                'message' => 'You are not authorized to add strikethrough text',
439            ];
440        }
441
442        /** Hyperlink */
443        $hyperlink = $this->getFeatureByNewKey($current_subscription, 'hyperlink', false);
444        $has_hyperlink_html = $this->hasTag('/<a(.+?)<\/a>/m', $html);
445
446        if ($has_hyperlink_html && ! $hyperlink) {
447            return [
448                'error' => true,
449                'message' => 'You are not authorized to add hyperlink',
450            ];
451        }
452
453        /** Alignment - Left */
454        $alignment_left = $this->getFeatureByNewKey($current_subscription, 'alignment', false);
455        $alignment_left_exists = $this->checkIfStyleExists('text-align', 'left', $html_styles);
456
457        if ($alignment_left_exists && ! in_array('left', $alignment_left)) {
458            return [
459                'error' => true,
460                'message' => 'You are not authorized to add left alignment text',
461            ];
462        }
463
464        /** Alignment - Centered */
465        $alignment_center = $this->getFeatureByNewKey($current_subscription, 'alignment', false);
466        $alignment_center_exists = $this->checkIfStyleExists('text-align', 'center', $html_styles);
467
468        if ($alignment_center_exists && ! in_array('center', $alignment_center)) {
469            return [
470                'error' => true,
471                'message' => 'You are not authorized to add center alignment text',
472            ];
473        }
474
475        /** Alignment - Right */
476        $alignment_right = $this->getFeatureByNewKey($current_subscription, 'alignment', false);
477        $alignment_right_exists = $this->checkIfStyleExists('text-align', 'right', $html_styles);
478
479        if ($alignment_right_exists && ! in_array('right', $alignment_right)) {
480            return [
481                'error' => true,
482                'message' => 'You are not authorized to add right alignment text',
483            ];
484        }
485
486        /** Font Size */
487        $font_size = $this->getFeatureByNewKey($current_subscription, 'font_size', false);
488        if (is_array($font_size)) {
489            if (array_key_exists('font-size', $html_styles)) {
490                $inputString = $html_styles['font-size'][0] ?: '10pt';
491                if (preg_match('/\d+/', $inputString, $matches)) {
492                    $extractedNumber = (int) $matches[0];
493                } else {
494                    $extractedNumber = null;
495                }
496            } else {
497                $extractedNumber = 10;
498            }
499
500            if (! empty($font_size) && $extractedNumber !== null && array_key_exists('font-size', $html_styles) && ! in_array($extractedNumber, $font_size)) {
501                return [
502                    'error' => true,
503                    'message' => 'Exceeds font size limit, use 10, 11 or 12pt size or upgrade plan '.$extractedNumber.'px',
504                ];
505            }
506        } else {
507            $font_size_match = $this->checkIfStyleExists('font-size', $font_size.'px', $html_styles);
508
509            if ($font_size != -1 && ! $font_size_match && array_key_exists('font-size', $html_styles)) {
510                return [
511                    'error' => true,
512                    'message' => 'Exceeds font size limit, use 10, 11 or 12pt size or upgrade plan '.$font_size.'px',
513                ];
514            }
515        }
516
517        /** Font Family */
518        $font = $this->getFeatureByNewKey($current_subscription, 'font_family', false);
519        if (is_array($font)) {
520            if (array_key_exists('font-family', $html_styles)) {
521                $extractedFontFamily = strtolower($html_styles['font-family'][0]);
522                $extractedFonts = array_map('trim', explode(',', $extractedFontFamily));
523                $hasDisallowedFont = ! empty(array_diff($extractedFonts, $font));
524                if ($hasDisallowedFont) {
525                    return [
526                        'error' => true,
527                        'message' => 'You are not authorized to add a font family other than Arial, Sans Serif and Calibri.',
528                    ];
529                }
530            }
531        } else {
532            if ($font != -1 && $font) {
533                $count_unique_fonts = $this->countStyles('font-family', $html_styles);
534                if ($count_unique_fonts > $font) {
535                    return [
536                        'error' => true,
537                        'message' => Str::replaceArray('?', [$font], 'You can not add more than ? font per flycut with current plan'),
538                    ];
539                }
540            }
541        }
542
543        /** Bullet Points */
544        $bullet_points = $this->getFeatureByNewKey($current_subscription, 'bullet_points', false);
545        $has_bullet_points_html = $this->hasTag('/<ul(.+?)<\/ul>/m', $html);
546
547        if ($has_bullet_points_html && ! $bullet_points) {
548            return [
549                'error' => true,
550                'message' => 'You are not authorized to add Bullet Points',
551            ];
552        }
553
554        /** Numbered List */
555        $numbered_list = $this->getFeatureByNewKey($current_subscription, 'numbered_list', false);
556        $has_numbered_list = $this->hasTag('/<ol(.+?)<\/ol>/m', $html);
557
558        if ($has_numbered_list && ! $numbered_list) {
559            return [
560                'error' => true,
561                'message' => 'You are not authorized to add Numbered List',
562            ];
563        }
564
565        /** Increase Indent */
566        $increase_indent = $this->getFeatureByNewKey($current_subscription, 'indent', false);
567        $has_increase_indent = array_key_exists('padding-left', $html_styles);
568
569        if ($has_increase_indent && ! $increase_indent) {
570            return [
571                'error' => true,
572                'message' => 'You are not authorized to add Increase Indent',
573            ];
574        }
575
576        return true;
577    }
578
579    /**
580     * Check if the user has permission to add Giphy GIFs.
581     *
582     * @param  string  $html
583     * @return array{error: bool, message: string}|true
584     */
585    protected function checkIfGiphyExists(Request $request, $html)
586    {
587        $html = str_replace("\n", '', str_replace(PHP_EOL, '', $html));
588
589        $current_subscription = $this->getCurrentPlan($request->user());
590        $allow_giphy = $this->getFeatureByNewKey($current_subscription, 'giphys', false);
591
592        $doc = new DOMDocument;
593        @$doc->loadHTML($html);
594        $imageTags = $doc->getElementsByTagName('img');
595
596        $gifs = collect($imageTags)->filter(function ($tag) {
597            return (pathinfo(parse_url($tag->getAttribute('src'), PHP_URL_PATH), PATHINFO_EXTENSION) == 'gif')
598                && ($this->getDomainName($tag->getAttribute('src')) == 'media2.giphy.com');
599        });
600
601        if (! $allow_giphy && $gifs->isNotEmpty()) {
602            return [
603                'error' => true,
604                'message' => 'You are not allowed to add giphy with current plan',
605            ];
606        }
607
608        return true;
609    }
610
611    /**
612     * Extract domain name from a URL.
613     *
614     * @param  string  $url
615     */
616    protected function getDomainName($url): string
617    {
618        $arr_url = parse_url($url);
619
620        return $arr_url['host'] ?? '';
621    }
622
623    /**
624     * Check if the user is within the character limit for FlyCuts.
625     *
626     * @param  string  $html
627     * @return array{error: bool, message: string}|true
628     */
629    protected function checkCharacterCount(Request $request, $html)
630    {
631        $current_subscription = $this->getCurrentPlan($request->user());
632        $user = $request->user();
633
634        $flycut_char_limit = $this->getFeatureByNewKey(
635            $current_subscription,
636            'character_limit',
637            0
638        );
639        $char_count = strlen($html);
640
641        $is_rewardable = (isset($user->rewardable)) ? $user->rewardable : '';
642        $rewards_level = (isset($user->rewards_level)) ? $user->rewards_level : '';
643
644        if ($is_rewardable) {
645            if ($rewards_level >= 2) {
646                $flycut_char_limit = -1;
647            }
648        }
649
650        if ($char_count > $flycut_char_limit && $flycut_char_limit > -1) {
651            return [
652                'error' => true,
653                'message' => Str::replaceArray('?', [$flycut_char_limit], 'You can not add more than ? character per flycut with current plan'),
654            ];
655        }
656
657        return true;
658    }
659
660    /**
661     * Check if the user is within the FlyCuts creation limit.
662     *
663     * @return array{error: bool, message: string}|true
664     */
665    protected function checkFlyCutsCount(Request $request)
666    {
667        $current_subscription = $this->getCurrentPlan($request->user());
668        $user = $request->user();
669
670        $total_allowed_shortcuts = $this->getFeatureByNewKey(
671            $current_subscription,
672            'shortcuts',
673            0
674        );
675
676        $is_rewardable = (isset($user->rewardable)) ? $user->rewardable : '';
677        $rewards_level = (isset($user->rewards_level)) ? $user->rewards_level : '';
678
679        if ($is_rewardable) {
680            if ($rewards_level >= 1) {
681                $total_allowed_shortcuts = -1;
682            }
683        }
684
685        $number_of_flyCuts_that_can_be_created = $total_allowed_shortcuts;
686        $current_flycuts_created = Shortcut::where('user_id', $user->id)->count();
687
688        if ($current_flycuts_created >= $number_of_flyCuts_that_can_be_created && $number_of_flyCuts_that_can_be_created > -1) {
689            return [
690                'error' => true,
691                'message' => 'You have reached the limit. The number of flycuts that can be created with the current plan has been exceeded.',
692            ];
693        }
694
695        return true;
696    }
697
698    /**
699     * Check if the user has permission to perform advanced search.
700     *
701     * @return array{error: bool, message: string}|true
702     */
703    protected function restrictAdvacedSearch(Request $request)
704    {
705        $current_subscription = $this->getCurrentPlan($request->user());
706        $search_bar = $this->getFeatureByNewKey($current_subscription, 'search_bar', false);
707
708        if ($search_bar !== 'advanced') {
709            return [
710                'error' => true,
711                'message' => 'You are not allowed to perform advanced search with current plan',
712            ];
713        }
714
715        return true;
716    }
717
718    /**
719     * Check if the user is within the categories creation limit.
720     *
721     * @return array{error: bool, message: string}|true
722     */
723    protected function checkCategoriesCount(Request $request)
724    {
725        $current_subscription = $this->getCurrentPlan($request->user());
726        $user = $request->user();
727
728        $total_allowed_categories = $this->getFeatureByNewKey($current_subscription, 'categories_count', 0);
729
730        $is_rewardable = (isset($user->rewardable)) ? $user->rewardable : '';
731        $rewards_level = (isset($user->rewards_level)) ? $user->rewards_level : '';
732
733        if ($is_rewardable) {
734            if ($rewards_level >= 3) {
735                if (
736                    $current_subscription['identifier'] == Plans::FREEMIUM_IDENTIFIER ||
737                    $current_subscription['identifier'] == Plans::STARTER_MONTHLY_IDENTIFIER ||
738                    $current_subscription['identifier'] == Plans::STARTER_YEARLY_IDENTIFIER
739                ) {
740                    $total_allowed_categories = $total_allowed_categories + 1;
741                }
742            }
743        }
744
745        $categories_limit = $total_allowed_categories ?? 0;
746        $categories_count = ShortcutCategory::where('user_id', request()->user()->id)->count();
747
748        if ($categories_count >= $categories_limit && $categories_limit != -1 && ! $request->category_id) {
749            return [
750                'error' => true,
751                'message' => Str::replaceArray('?', [$categories_limit], 'You can not add more than ? categories with current plan'),
752            ];
753        }
754
755        return true;
756    }
757
758    /**
759     * Check if the user is within the subcategories creation limit.
760     *
761     * @return array{error: bool, message: string}|true
762     */
763    protected function checkSubCategoriesCount(Request $request)
764    {
765        $current_subscription = $this->getCurrentPlan($request->user());
766
767        $sub_categories_limit = $this->getFeatureByNewKey($current_subscription, 'subcategories_count', 0);
768        $sub_categories_count = ShortcutSubCategoryLv1::whereHas('ShortcutCategory')->where('user_id', request()->user()->id)->count();
769
770        if ($sub_categories_count >= $sub_categories_limit && $sub_categories_limit != -1) {
771            return [
772                'error' => true,
773                'message' => Str::replaceArray('?', [$sub_categories_limit], 'You can not add more than ? sub categories with current plan'),
774            ];
775        }
776
777        return true;
778    }
779
780    /**
781     * Check if the user has permission to use FlyPlates.
782     *
783     * @return array{error: bool, message: string}|true
784     */
785    protected function checkFlyPlates(Request $request)
786    {
787        $user = $request->user();
788
789        $is_rewardable = (isset($user->rewardable)) ? $user->rewardable : '';
790        $rewards_level = (isset($user->rewards_level)) ? $user->rewards_level : '';
791
792        if (! $is_rewardable || $rewards_level < 4) {
793            $template_id = $request->template_id;
794            $categorydata = TemplateCategory::with('templates')
795                ->whereHas('templates', function ($q) use ($template_id) {
796                    $searchArray = ['_id' => $template_id];
797
798                    return $q->where($searchArray);
799                })
800                ->first();
801
802            if (! empty($categorydata) && (isset($categorydata->name)) && $categorydata->name != 'General') {
803                $current_subscription = $this->getCurrentPlan($request->user());
804
805                $can_access_flyplates = $this->getFeatureByNewKey($current_subscription, 'templates', false);
806
807                if (! $can_access_flyplates) {
808                    return [
809                        'error' => true,
810                        'message' => 'You are not authorized to add flyplates',
811                    ];
812                }
813
814                $flyplates_count = Shortcut::where(['user_defined' => false])->count();
815
816                if ($flyplates_count > $can_access_flyplates && $can_access_flyplates != -1) {
817                    return [
818                        'error' => true,
819                        'message' => Str::replaceArray('?', [$can_access_flyplates], 'You can not create more than ? flycut from flyplates with current plan'),
820                    ];
821                }
822            }
823        }
824
825        return true;
826    }
827
828    /**
829     * Check if the user is within the shortcut version rollback limit.
830     *
831     * @return array{error: bool, message: string}|true
832     */
833    protected function checkShortcutVersionRollBackCount(Request $request)
834    {
835        $current_subscription = $this->getCurrentPlan($request->user());
836
837        $rollback_limit = $this->getFeatureByNewKey($current_subscription, 'version_history_limit', 0);
838        $rollback_counts = Shortcut::where('_id', $request->shortcutId)->pluck('rollback_counts')->first();
839
840        Log::info('rollback_limit', [
841            'rollback_limit' => $rollback_limit,
842            'rollback_counts' => $rollback_counts,
843            'current_subscription' => $current_subscription,
844            'identifier' => $current_subscription['identifier'],
845        ]);
846
847        if ($rollback_limit == 0) {
848            return [
849                'error' => true,
850                'message' => 'You are not authorized to roll back shortcut versions',
851            ];
852        }
853
854        if ($rollback_counts && ($rollback_counts >= $rollback_limit) && ($rollback_limit != -1)) {
855            return [
856                'error' => true,
857                'message' => Str::replaceArray('?', [$rollback_limit], 'You can not rollback more than ? shortcut versions'),
858            ];
859        }
860
861        return true;
862    }
863
864    /**
865     * Retrieve coupon object from Stripe.
866     *
867     * @return mixed
868     */
869    private function retrieveCouponObject(string $couponCode)
870    {
871        $stripe = new StripeClient(config('services.stripe.secret'));
872
873        return $stripe->coupons->retrieve($couponCode, ['expand' => ['applies_to']]);
874    }
875}