Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.56% covered (success)
95.56%
129 / 135
66.67% covered (warning)
66.67%
4 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
MetaDataController
95.56% covered (success)
95.56%
129 / 135
66.67% covered (warning)
66.67%
4 / 6
29
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
 buildFeatureFlags
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 checkQuota
100.00% covered (success)
100.00%
51 / 51
100.00% covered (success)
100.00%
1 / 1
11
 applyParameterDefaults
93.75% covered (success)
93.75%
30 / 32
0.00% covered (danger)
0.00%
0 / 1
8.02
 publicMetaData
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
3
 checkOnlyQuota
85.71% covered (warning)
85.71%
24 / 28
0.00% covered (danger)
0.00%
0 / 1
4.05
1<?php
2
3namespace App\Http\Controllers\v2;
4
5use App\Http\Controllers\Controller;
6use App\Http\Models\FlyGrammarLanguage;
7use App\Http\Services\FeatureFlagService;
8use App\Http\Services\InAppNotificationService;
9use App\Http\Services\ParameterService;
10use App\Http\Services\RemoteConfigService;
11use App\Http\Services\UserFieldInjectionService;
12use App\Http\Services\WPSService;
13use App\Services\FlyMsgAI\FlyMsgAIService;
14use App\Traits\SubscriptionTrait;
15use Illuminate\Http\JsonResponse;
16use Illuminate\Http\Request;
17
18class MetaDataController extends Controller
19{
20    use SubscriptionTrait;
21
22    public function __construct(
23        private readonly FlyMsgAIService $ai_service,
24        private readonly WPSService $wpsService,
25        private readonly ParameterService $parameterService,
26        private readonly RemoteConfigService $remoteConfigService,
27        private readonly UserFieldInjectionService $userFieldInjectionService,
28        private readonly InAppNotificationService $inAppNotificationService,
29        private readonly FeatureFlagService $featureFlagService,
30    ) {}
31
32    /**
33     * Feature flag keys exposed to frontends via the meta-data endpoint.
34     *
35     * The frontends read `features[key]` to decide whether to render the
36     * corresponding UI surface. Keep this list small — it ships on every
37     * meta-data call.
38     */
39    private const EXPOSED_FEATURE_FLAGS = [
40        FeatureFlagService::ROLEPLAY_CORPORATE_PERSONAS,
41    ];
42
43    /**
44     * Build the public feature flag map for the meta-data payload.
45     *
46     * @return array<string, bool>
47     */
48    private function buildFeatureFlags(): array
49    {
50        $out = [];
51        foreach (self::EXPOSED_FEATURE_FLAGS as $key) {
52            $out[$key] = $this->featureFlagService->enabled($key);
53        }
54
55        return $out;
56    }
57
58    public function checkQuota(Request $request): JsonResponse
59    {
60        $user = $request->user();
61
62        $currentSubscriptionPlan = $this->getCurrentPlan($user);
63        $quota = $this->getFlyAIQuota($user);
64        $flycuts_quota = $this->getFlyCutsQuota($user);
65        $promptsQuota = $this->getPromptQuota($user);
66        $grammarQuota = $this->getGrammarQuota($user);
67
68        $flyGrammarLanguagesAvailable = FlyGrammarLanguage::select(['value', 'description'])->orderBy('index')
69            ->get()->map(function ($item) {
70                return [
71                    'id' => $item->value,
72                    'value' => $item->description,
73                ];
74            })->toArray();
75
76        $plan = $this->getCurrentPlan($user);
77        $show_upgrade_button = empty($user->company_id) && $plan->identifier !== 'sales-pro-monthly' && $plan->identifier !== 'sales-pro-yearly';
78
79        $meta = [
80            'is_enterprise' => ! empty($user->company_id) && $user->status !== 'Invited',
81            'show_upgrade_button' => $show_upgrade_button,
82            'quota' => $quota,
83            'prompts_quota' => $promptsQuota,
84            'flycuts_quota' => $flycuts_quota,
85            'grammar_quota' => $grammarQuota,
86            'plan' => $currentSubscriptionPlan,
87            'fly_grammar_languages_available' => $flyGrammarLanguagesAvailable,
88            'user_dictionary' => $this->wpsService->getUserDictionary($user),
89        ];
90
91        // Load all metadata parameters dynamically (single cached query)
92        $metadataParams = $this->parameterService->getMetadataParameters();
93        foreach ($metadataParams as $param) {
94            $meta[$param->metadata_key] = $param->value;
95        }
96
97        // Expose per-user developer mode flag so the client can override global remote config
98        $meta['developer_mode'] = (bool) ($user->developer_mode ?? false);
99
100        // Public feature flag surface: a small map of dark-launched toggles the
101        // frontends consult before rendering feature-gated UI.
102        $meta['features'] = $this->buildFeatureFlags();
103
104        // Apply special handling for parameters with default fallbacks
105        $this->applyParameterDefaults($meta);
106
107        // Grammar quota override: disable autocorrect/autocomplete when quota is limited
108        if ($grammarQuota['remaining'] >= 0 && $grammarQuota['total'] !== -1) {
109            $meta['wsc_autocorrect'] = false;
110            $meta['wsc_autocomplete'] = false;
111        }
112
113        $remoteConfig = $this->remoteConfigService->getForMetaData();
114        if ($remoteConfig !== null) {
115            $remoteConfig = $this->userFieldInjectionService->mergeWithGlobalConfig(
116                $remoteConfig, $user->_id
117            );
118            $meta['remote_config'] = $remoteConfig;
119        } else {
120            $userOverrides = $this->userFieldInjectionService->getUserFieldInjectionFresh($user->_id);
121            if ($userOverrides && ! empty($userOverrides->field_injection)) {
122                $meta['remote_config'] = ['field_injection' => $userOverrides->field_injection];
123            }
124        }
125
126        $notifications = $this->inAppNotificationService->getAndMarkForUser(
127            (string) $user->_id,
128            $currentSubscriptionPlan['identifier'] ?? ''
129        );
130        if (! empty($notifications)) {
131            $meta['in_app_notifications'] = $notifications;
132        }
133
134        return response()->json($meta);
135    }
136
137    /**
138     * Apply default values for parameters that require special handling.
139     *
140     * @param  array<string, mixed>  $meta  The metadata array to modify
141     */
142    private function applyParameterDefaults(array &$meta): void
143    {
144        // fly_grammar_update_prompt has a default message if not set
145        if (! isset($meta['fly_grammar_update_prompt']) || $meta['fly_grammar_update_prompt'] === null) {
146            $meta['fly_grammar_update_prompt'] = ['Work smarter, not harder.'];
147        }
148
149        // fly_grammar_disabled_on_flymsg defaults to false if not set
150        if (! isset($meta['fly_grammar_disabled_on_flymsg'])) {
151            $meta['fly_grammar_disabled_on_flymsg'] = false;
152        }
153
154        // fly_cut_update_coupons has a default coupon array if not set
155        if (isset($meta['fly_cut_update_coupons']) && $meta['fly_cut_update_coupons'] !== null) {
156            $meta['fly_cut_update_coupons'] = collect($meta['fly_cut_update_coupons'])->sortBy('order')->values()->all();
157        } else {
158            $meta['fly_cut_update_coupons'] = [
159                [
160                    'title' => 'Never Hit a Limit Again!',
161                    'description' => "You've hit your {quota} daily text expansion limit. Your next refill is in {next_refill}. See how many times we've deployed a FlyCut:",
162                    'code' => '30OFF',
163                    'cta' => 'Get 30% Off on Year 1!',
164                    'cta_instruction' => 'On Page 2 of Checkout, Use this Code (click code to copy):',
165                    'cta_url' => config('romeo.frontend-base-url').'/new-plan/YearlyGrowth',
166                    'color' => '#2A1E36',
167                    'order' => 1,
168                ],
169            ];
170        }
171
172        // fly_grammar_update_coupons has a default coupon array if not set
173        if (isset($meta['fly_grammar_update_coupons']) && $meta['fly_grammar_update_coupons'] !== null) {
174            $meta['fly_grammar_update_coupons'] = collect($meta['fly_grammar_update_coupons'])->sortBy('order')->values()->all();
175        } else {
176            $meta['fly_grammar_update_coupons'] = [
177                [
178                    'title' => 'Unlock Unlimited Grammar Perfection!',
179                    'description' => "You've hit your {quota} daily spelling and grammar fixes. Your next refill is in {next_refill}. See how many improvements we've helped you make:",
180                    'code' => '30OFF',
181                    'cta' => 'Get 30% Off on Year 1!',
182                    'cta_instruction' => 'On Page 2 of Checkout, Use this Code (click code to copy):',
183                    'cta_url' => config('romeo.frontend-base-url').'/new-plan/YearlyGrowth',
184                    'color' => '#F15A2A',
185                    'order' => 1,
186                ],
187            ];
188        }
189    }
190
191    /**
192     * Get public metadata that does not require authentication.
193     *
194     * Returns general, non-user-specific configuration data intended for
195     * use by the extension before a user session is established.
196     * Includes grammar languages, metadata parameters, parameter defaults,
197     * and the global remote config (without user field injection overrides).
198     */
199    public function publicMetaData(): JsonResponse
200    {
201        $flyGrammarLanguagesAvailable = FlyGrammarLanguage::select(['value', 'description'])->orderBy('index')
202            ->get()->map(function ($item) {
203                return [
204                    'id' => $item->value,
205                    'value' => $item->description,
206                ];
207            })->toArray();
208
209        $meta = [
210            'fly_grammar_languages_available' => $flyGrammarLanguagesAvailable,
211            'features' => $this->buildFeatureFlags(),
212        ];
213
214        $metadataParams = $this->parameterService->getMetadataParameters();
215        foreach ($metadataParams as $param) {
216            $meta[$param->metadata_key] = $param->value;
217        }
218
219        $this->applyParameterDefaults($meta);
220
221        $remoteConfig = $this->remoteConfigService->getForMetaData();
222        if ($remoteConfig !== null) {
223            $meta['remote_config'] = $remoteConfig;
224        }
225
226        return response()->json($meta);
227    }
228
229    public function checkOnlyQuota(Request $request): JsonResponse
230    {
231        $user = $request->user();
232
233        $currentSubscriptionPlan = $this->getCurrentPlan($user);
234        $quota = $this->getFlyAIQuota($user);
235        $flycuts_quota = $this->getFlyCutsQuota($user);
236        $promptsQuota = $this->getPromptQuota($user);
237        $grammarQuota = $this->getGrammarQuota($user);
238
239        $flyGrammarLanguagesAvailable = FlyGrammarLanguage::select(['value', 'description'])->orderBy('index')
240            ->get()->map(function ($item) {
241                return [
242                    'id' => $item->value,
243                    'value' => $item->description,
244                ];
245            })->toArray();
246
247        $plan = $this->getCurrentPlan($user);
248        $show_upgrade_button = empty($user->company_id) && $plan->identifier !== 'sales-pro-monthly' && $plan->identifier !== 'sales-pro-yearly';
249
250        $meta = [
251            'is_enterprise' => ! empty($user->company_id) && $user->status !== 'Invited',
252            'show_upgrade_button' => $show_upgrade_button,
253            'quota' => $quota,
254            'prompts_quota' => $promptsQuota,
255            'flycuts_quota' => $flycuts_quota,
256            'grammar_quota' => $grammarQuota,
257            'developer_mode' => (bool) ($user->developer_mode ?? false),
258            'plan' => $currentSubscriptionPlan,
259            'fly_grammar_languages_available' => $flyGrammarLanguagesAvailable,
260            'user_dictionary' => $this->wpsService->getUserDictionary($user),
261        ];
262
263        return response()->json($meta);
264    }
265}