Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.85% covered (success)
97.85%
91 / 93
77.78% covered (warning)
77.78%
7 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
GlobalSettingsService
97.85% covered (success)
97.85%
91 / 93
77.78% covered (warning)
77.78%
7 / 9
14
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCacheTtl
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 exists
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 create
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 update
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 delete
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 invalidateCache
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDefaults
100.00% covered (success)
100.00%
65 / 65
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace App\Http\Services;
4
5use App\Http\Models\FlyCutUsage;
6use App\Http\Models\Setting;
7use App\Http\Repositories\GlobalSettingsRepository;
8use Illuminate\Support\Facades\Cache;
9use Illuminate\Support\Facades\Config;
10
11/**
12 * Service for global settings business logic.
13 *
14 * This service handles all business logic related to global settings,
15 * coordinating with the repository for data access. Global settings is
16 * a single record that contains application-wide configuration defaults.
17 * Includes Redis caching for improved read performance.
18 */
19class GlobalSettingsService
20{
21    /**
22     * Cache key for global settings.
23     */
24    private const CACHE_KEY = 'global_settings';
25
26    public function __construct(
27        private GlobalSettingsRepository $globalSettingsRepository,
28        private CacheInvalidationService $cacheInvalidationService
29    ) {}
30
31    /**
32     * Get the cache TTL from config.
33     */
34    private function getCacheTtl(): int
35    {
36        return Config::get('cache.ttl.global_settings', 3600);
37    }
38
39    /**
40     * Get the global settings.
41     *
42     * Results are cached for improved read performance.
43     *
44     * @return Setting|null The global settings or null if not found
45     */
46    public function get(): ?Setting
47    {
48        return Cache::remember(self::CACHE_KEY, $this->getCacheTtl(), function () {
49            return $this->globalSettingsRepository->find();
50        });
51    }
52
53    /**
54     * Check if global settings exist.
55     *
56     * @return bool True if global settings exist
57     */
58    public function exists(): bool
59    {
60        return $this->globalSettingsRepository->exists();
61    }
62
63    /**
64     * Create global settings with defaults merged with provided data.
65     *
66     * If global settings already exist, they will be deleted and recreated.
67     *
68     * @param  array<string, mixed>  $data  Optional custom data to merge with defaults
69     * @return Setting The created settings
70     */
71    public function create(array $data = []): Setting
72    {
73        // Delete existing global settings if they exist
74        $existing = $this->globalSettingsRepository->find();
75        if ($existing) {
76            $this->globalSettingsRepository->delete($existing);
77        }
78
79        // Merge provided data with defaults
80        $settingsData = array_merge($this->getDefaults(), $data);
81
82        $setting = $this->globalSettingsRepository->create($settingsData);
83
84        $this->cacheInvalidationService->invalidateGlobalSettingsCaches();
85
86        return $setting;
87    }
88
89    /**
90     * Update the global settings.
91     *
92     * Supports partial updates. For nested objects like 'features',
93     * provided values are merged with existing values.
94     *
95     * @param  array<string, mixed>  $data  The update data
96     * @return Setting|null The updated settings or null if not found
97     */
98    public function update(array $data): ?Setting
99    {
100        $setting = $this->globalSettingsRepository->find();
101
102        if (! $setting) {
103            return null;
104        }
105
106        // Handle partial update for features (merge with existing)
107        if (isset($data['features']) && is_array($setting->features)) {
108            $data['features'] = array_merge($setting->features, $data['features']);
109        }
110
111        $setting = $this->globalSettingsRepository->update($setting, $data);
112
113        $this->cacheInvalidationService->invalidateGlobalSettingsCaches();
114
115        return $setting;
116    }
117
118    /**
119     * Delete the global settings.
120     *
121     * @return bool True if deleted successfully, false if not found
122     */
123    public function delete(): bool
124    {
125        $setting = $this->globalSettingsRepository->find();
126
127        if (! $setting) {
128            return false;
129        }
130
131        $result = $this->globalSettingsRepository->delete($setting);
132
133        $this->cacheInvalidationService->invalidateGlobalSettingsCaches();
134
135        return $result;
136    }
137
138    /**
139     * Invalidate the global settings cache.
140     *
141     * Useful for manual cache clearing.
142     */
143    public function invalidateCache(): void
144    {
145        $this->cacheInvalidationService->invalidateGlobalSettingsCaches();
146    }
147
148    /**
149     * Get the default global settings values.
150     *
151     * @return array<string, mixed> The default settings
152     */
153    private function getDefaults(): array
154    {
155        return [
156            'remove_grammar_on_lose_focus' => false,
157            'wage_per_hour' => FlyCutUsage::WAGE_PER_HOUR,
158            'words_per_minute' => FlyCutUsage::WORDS_PER_MINUTE,
159            'typing_style' => 'all',
160            'typing_speed' => 45,
161            'fly_rewrite' => 'enabled',
162            'fly_grammar' => 'enabled',
163            'fly_grammar_default_language' => 'en_US',
164            'features' => [
165                'flyCuts' => true,
166                'flyGrammar' => true,
167                'gmailPlugin' => true,
168                'linkedinPlugin' => true,
169                'flyPosts' => true,
170                'flyEngage' => true,
171                'outlookPlugin' => true,
172                'developer_mode' => false,
173            ],
174            'playlists' => [
175                'FE' => [
176                    'title' => 'FlyEngage AI',
177                    'link' => 'https://www.youtube.com/embed/videoseries?si=qxFJZaQEckCCghkI&list=PLNswoLQcNQEIDimmUYa05VHvuYIRsh-HX&enablejsapi=1',
178                    'description' => 'How to Write a Comment on LinkedIn with AI using FlyEngage AI in under 30 Seconds!',
179                ],
180                'FP' => [
181                    'title' => 'FlyPosts AI',
182                    'link' => 'https://www.youtube.com/embed/3ycEO-hPkZo?si=g58yHWwzJi2bvlFP;list=PLNswoLQcNQEKFpMBiotZ45M5uzfEZbTLx&enablejsapi=1',
183                    'description' => 'How to Leverage FlyPosts AI - an AI Social Post Generator',
184                ],
185            ],
186            'ignoredInputTypes' => [
187                'password', 'checkbox', 'radio', 'file', 'button', 'submit', 'reset',
188                'select-one', 'select-multiple', 'range', 'image', 'email', 'number',
189                'hidden', 'time', 'url', 'week', 'tel', 'search', 'month', 'color',
190                'datetime-local', 'date', 'datetime',
191            ],
192            'blackListClasses' => [
193                'appsElementsSidekickPromptInput', 'fly-grammar-ignore', 'wsc-ignore',
194                'disabled', 'hidden', 'ql-clipboard', 'fly-rewrite-ignore', 'fly-msg-ignore',
195            ],
196            'blackListIds' => [':11s'],
197            'blackListAttributes' => [
198                ['name' => 'aria-label', 'value' => 'To recipients'],
199            ],
200            'blackListDomainRules' => [
201                [
202                    'domain' => 'calendar.google.com',
203                    'blackListClasses' => [],
204                    'blackListIds' => [],
205                    'blackListAttributes' => [
206                        ['name' => 'placeholder', 'value' => 'hh:mm'],
207                        ['name' => 'aria-label', 'value' => 'Start date'],
208                        ['name' => 'aria-label', 'value' => 'Guests'],
209                        ['name' => 'placeholder', 'value' => 'Add location'],
210                    ],
211                ],
212                [
213                    'domain' => 'linkedin.com',
214                    'blackListClasses' => ['search-global-typeahead__input', 'artdeco-typeahead__input'],
215                    'blackListIds' => [],
216                    'blackListAttributes' => [],
217                ],
218            ],
219        ];
220    }
221}