Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
18.18% |
68 / 374 |
|
14.29% |
4 / 28 |
CRAP | |
0.00% |
0 / 1 |
| SubscriptionTrait | |
18.18% |
68 / 374 |
|
14.29% |
4 / 28 |
9386.27 | |
0.00% |
0 / 1 |
| getPlanFeatureService | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| getFeatureByLegacyKey | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| getFeatureByNewKey | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| getCurrentPlan | |
85.71% |
18 / 21 |
|
0.00% |
0 / 1 |
8.19 | |||
| getCurrentAddOns | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
| getGrammarQuota | |
91.67% |
11 / 12 |
|
0.00% |
0 / 1 |
2.00 | |||
| getQuotaUsed | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| getFlyAIQuota | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
| getFlyCutsQuota | |
93.33% |
14 / 15 |
|
0.00% |
0 / 1 |
1.00 | |||
| getPromptQuota | |
90.00% |
9 / 10 |
|
0.00% |
0 / 1 |
1.00 | |||
| getCurrentSubscription | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
72 | |||
| extractStylesFromHtml | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
20 | |||
| hasTag | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| hasHyperlink | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
| checkIfStyleExists | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
| countStyles | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| getMediaStorage | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| checkStylesTagsPermissions | |
0.00% |
0 / 118 |
|
0.00% |
0 / 1 |
1640 | |||
| checkIfGiphyExists | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
20 | |||
| getDomainName | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| checkCharacterCount | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
56 | |||
| checkFlyCutsCount | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
56 | |||
| restrictAdvacedSearch | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
| checkCategoriesCount | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
132 | |||
| checkSubCategoriesCount | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
| checkFlyPlates | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
132 | |||
| checkShortcutVersionRollBackCount | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
30 | |||
| retrieveCouponObject | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Traits; |
| 4 | |
| 5 | use App\Helpers\Constants; |
| 6 | use App\Http\Models\Auth\User; |
| 7 | use App\Http\Models\FlyGrammarActions; |
| 8 | use App\Http\Models\FlyMsgUserDailyUsage; |
| 9 | use App\Http\Models\Plans; |
| 10 | use App\Http\Models\Prompts\CustomPrompts; |
| 11 | use App\Http\Models\Shortcut; |
| 12 | use App\Http\Models\ShortcutCategory; |
| 13 | use App\Http\Models\ShortcutSubCategoryLv1; |
| 14 | use App\Http\Models\TemplateCategory; |
| 15 | use App\Http\Models\UserAddOns; |
| 16 | use App\Http\Services\PlanFeatureService; |
| 17 | use App\Traits\AccountCenter\Reporting\FlyMsgAITrackingTrait; |
| 18 | use Carbon\Carbon; |
| 19 | use DOMDocument; |
| 20 | use Illuminate\Http\Request; |
| 21 | use Illuminate\Support\Facades\Log; |
| 22 | use Illuminate\Support\Str; |
| 23 | use MongoDB\BSON\UTCDateTime; |
| 24 | use 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 | */ |
| 33 | trait 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 | } |