Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 136
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
SubscriptionController
0.00% covered (danger)
0.00%
0 / 136
0.00% covered (danger)
0.00%
0 / 8
1122
0.00% covered (danger)
0.00%
0 / 1
 index
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
2
 current
0.00% covered (danger)
0.00%
0 / 72
0.00% covered (danger)
0.00%
0 / 1
210
 isOnPremiumPlan
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 cancelSubscription
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 resumeSubscription
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 getInvoices
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCoupon
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 trialSubscription
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2
3namespace App\Http\Controllers\v1\Stripe;
4
5use App\Http\Controllers\Controller;
6use App\Http\Models\Admin\CompanyLicenses;
7use App\Http\Models\Auth\User;
8use App\Http\Models\Plans;
9use App\Http\Models\Shortcut;
10use App\Http\Models\ShortcutCategory;
11use App\Http\Models\ShortcutSubCategoryLv1;
12use App\Http\Models\UserReferral;
13use App\Http\Requests\ValidateCouponRequest;
14use App\Traits\SubscriptionTrait;
15use Carbon\Carbon;
16use Exception;
17use Illuminate\Http\JsonResponse;
18use Illuminate\Http\Request;
19use Stripe\StripeClient;
20
21class SubscriptionController extends Controller
22{
23    use SubscriptionTrait;
24
25    public function index(Request $request): JsonResponse
26    {
27        $plans = Plans::select('title', 'stripe_obj', 'identifier', 'currency', 'interval', 'unit_amount')
28            ->with('planFeatures.feature')
29            ->whereNull('pricing_version')
30            ->get();
31
32        $data = [];
33
34        $plans->each(function ($plan) use (&$data) {
35            $data[] = [
36                'title' => $plan->title,
37                'identifier' => $plan->identifier,
38                'features' => $this->getPlanFeatureService()->getAllFeaturesLegacyFormat($plan),
39                'currency' => $plan->currency,
40                'interval' => $plan->interval,
41                'unit_amount' => $plan->unit_amount,
42            ];
43        });
44
45        return response()->json($data);
46    }
47
48    public function current(Request $request)
49    {
50        $user = $request->user();
51
52        $currentPlanModel = $this->getCurrentPlan($user);
53        $currentPlan = $currentPlanModel->toArray();
54        $planIdentifier = $currentPlan['identifier'];
55
56        // Get features using the new PlanFeatureService (returns legacy format)
57        $features = $this->getPlanFeatureService()->getAllFeaturesLegacyFormat($currentPlanModel, $user);
58
59        // User statistics as per their plan
60        $categoriesCount = ShortcutCategory::where('user_id', request()->user()->id)->count();
61        $subCategoriesCount = ShortcutSubCategoryLv1::whereHas('ShortcutCategory')->where('user_id', request()->user()->id)->count();
62        $flyplatesCount = Shortcut::where(['user_defined' => false])->count();
63        $flycutsCounts = Shortcut::count();
64        $totalAllowedCharacters = $features['Number of Characters that can be used per flycut'] ?? 0;
65        $spellCheck = $features['Spell Check (Language)'] ?? null;
66        $totalFlyplatesAllowed = $features['FlyPlates'] ?? 0;
67        $totalAllowedCategories = $features['Categories'] ?? 0;
68        $totalAllowedShortcuts = $features['Number of FlyCuts that can be created'] ?? 0;
69
70        // for flyrewards restrictions
71        $isRewardable = $user->rewardable ?? false;
72        $rewardsLevel = $user->rewards_level ?? 0;
73
74        if ($isRewardable) {
75            // Level 1+: Unlimited shortcuts
76            if ($rewardsLevel >= 1) {
77                $totalAllowedShortcuts = -1;
78                $features['Number of FlyCuts that can be created'] = $totalAllowedShortcuts;
79            }
80
81            // Level 2+: Unlimited characters
82            if ($rewardsLevel >= 2) {
83                $totalAllowedCharacters = -1;
84                $features['Number of Characters that can be used per flycut'] = $totalAllowedCharacters;
85            }
86
87            // Level 3+: Extra category for freemium/starter
88            if ($rewardsLevel >= 3) {
89                if (
90                    $planIdentifier == Plans::FREEMIUM_IDENTIFIER ||
91                    $planIdentifier == Plans::STARTER_MONTHLY_IDENTIFIER ||
92                    $planIdentifier == Plans::STARTER_YEARLY_IDENTIFIER
93                ) {
94                    $totalAllowedCategories = $totalAllowedCategories + 1;
95                    $features['Categories'] = $totalAllowedCategories;
96                }
97            }
98
99            // Level 4+: Unlimited templates
100            if ($rewardsLevel >= 4) {
101                $totalFlyplatesAllowed = -1;
102                $features['FlyPlates'] = $totalFlyplatesAllowed;
103            }
104        }
105
106        $currentPlan['features'] = $features;
107
108        $currentPlan['statistics']['sub_categories'] = $subCategoriesCount;
109        $currentPlan['statistics']['sub_categories_available'] = $features['Subcategory'] ?? 0;
110        $currentPlan['statistics']['categories'] = $categoriesCount;
111        $currentPlan['statistics']['categories_available'] = $totalAllowedCategories;
112        $currentPlan['statistics']['flyplates'] = $flyplatesCount;
113        $currentPlan['statistics']['flyplates_available'] = $totalFlyplatesAllowed;
114        $currentPlan['statistics']['flycuts'] = $flycutsCounts;
115        $currentPlan['statistics']['flycuts_available'] = $totalAllowedShortcuts;
116        $currentPlan['statistics']['characters'] = $totalAllowedCharacters;
117        $currentPlan['statistics']['spell_check_language'] = $spellCheck;
118
119        $currentPlan['referrals'] = UserReferral::firstWhere('user_id', $user->id);
120
121        $userId = $user->getKey();
122        $referralKey = $user->referral_key;
123        if (! $referralKey) {
124            $referralKey = uniqid();
125            User::where('_id', $userId)->update([
126                'referral_key' => $referralKey,
127            ]);
128        }
129        $currentPlan['referral_key'] = $referralKey;
130        $currentPlan['used_trial'] = $user->subscriptionTrials()->where('plan_identifier', 'sales-pro-monthly')->exists();
131        $currentPlan['on_trial'] = false;
132
133        $userDetails = User::withTrashed()->firstWhere('email', $user->email);
134        $userDetails['is_poc'] = $userDetails->isPOC();
135        $currentPlan['user_details'] = $userDetails;
136
137        $currentPlan['already_used_pro_trial'] = $user->subscriptionTrials()->exists();
138
139        if ($planIdentifier == Plans::FREEMIUM_IDENTIFIER) {
140            return $currentPlan;
141        }
142
143        $currentPlan['on_trial'] = $user->subscription('main')->onTrial();
144        $currentPlan['plan_status'] = $user->subscription('main')->stripe_status;
145        $planEndsOn = $currentPlan['on_trial'] ? $user->subscription('main')->trial_ends_at : $user->subscription('main')->ends_at ?? null;
146
147        if ($user->company_id) {
148            $companyLicense = CompanyLicenses::where('company_id', $user->company_id)
149                ->active()
150                ->first();
151
152            if ($companyLicense) {
153                $planEndsOn = Carbon::createFromFormat('Y-m-d H:i:s', $companyLicense->contract_end_date);
154            }
155        }
156
157        $currentPlan['ends_at'] = $planEndsOn;
158        $currentPlan['used_trial'] = $user->subscriptionTrials()->where('plan_identifier', $planIdentifier)->exists();
159
160        return $currentPlan;
161    }
162
163    public function isOnPremiumPlan(Request $request): JsonResponse
164    {
165        $current_subscription = $request->user()->subscription('main');
166
167        if ($current_subscription) {
168            return response()->json(true);
169        }
170
171        return response()->json(false);
172    }
173
174    public function cancelSubscription(Request $request): JsonResponse
175    {
176        try {
177            if (! $request->user()->subscribed('main')) {
178                throw new Exception('You are not subscribed to any plans');
179            }
180
181            $request->user()->subscription('main')->cancel();
182
183            return response()->json(['message' => 'You subscription has been successfully cancelled']);
184        } catch (Exception $e) {
185            return response()->json(['error' => $e->getMessage()], 422);
186        }
187    }
188
189    public function resumeSubscription(Request $request): JsonResponse
190    {
191        try {
192            if ($request->user()->subscribed('main') && ! $request->user()->subscription('main')->onGracePeriod()) {
193                throw new Exception('You are already subscribed to plan');
194            }
195
196            $request->user()->subscription('main')->resume();
197
198            return response()->json(['message' => 'You subscription has been successfully resumed']);
199        } catch (Exception $e) {
200            return response()->json(['error' => $e->getMessage()], 422);
201        }
202    }
203
204    public function getInvoices(Request $request): JsonResponse
205    {
206        return response()->json($request->user()->invoicesIncludingPending(), 200);
207    }
208
209    /**
210     * Retrieve coupon object
211     */
212    public function getCoupon(ValidateCouponRequest $request): JsonResponse
213    {
214        $plan = Plans::select('stripe_obj')->whereNull('pricing_version')->firstWhere('identifier', $request->plan_identifier);
215
216        try {
217            $coupon = $this->retrieveCouponObject($request->coupon);
218            $coupon->valid = in_array($plan->stripe_obj['product'] ?? '', $coupon->applies_to->products);
219            $coupon = $coupon->valid ? $coupon : ['valid' => false];
220
221            return response()->json($coupon);
222        } catch (\Exception $e) {
223            return response()->json(['message' => $e->getMessage()], 422);
224        }
225    }
226
227    public function trialSubscription(Request $request)
228    {
229        $user = $request->user();
230
231        if ($this->getCurrentPlan($user)->identifier !== Plans::FREEMIUM_IDENTIFIER) {
232            return response()->json(['message' => 'You are not eligible for a trial plan'], 422);
233        }
234
235        $planIdentifier = 'sales-pro-monthly';
236
237        if ($user->subscriptionTrials()->where('plan_identifier', $planIdentifier)->exists()) {
238            return response()->json(['message' => 'You have used your trial period for this plan already!'], 403);
239        }
240
241        $trialPeriodDays = 14;
242        $planId = Plans::select('stripe_id')->whereNull('pricing_version')->firstWhere('identifier', $planIdentifier)->stripe_id;
243
244        try {
245            if (! $user->hasStripeId()) {
246                $user->createAsStripeCustomer([
247                    'name' => $user->first_name.' '.$user->last_name,
248                    'email' => $user->email,
249                ]);
250            }
251
252            $stripe = new StripeClient(config('services.stripe.secret'));
253
254            $stripe->subscriptions->create([
255                'customer' => $user->stripe_id,
256                'items' => [['price' => $planId]],
257                'trial_period_days' => $trialPeriodDays,
258                'payment_settings' => ['save_default_payment_method' => 'on_subscription'],
259                'trial_settings' => ['end_behavior' => ['missing_payment_method' => 'cancel']],
260            ]);
261
262            return response()->json(['message' => "You have subscribed to {$planIdentifier} on trial for {$trialPeriodDays}"]);
263        } catch (Exception $e) {
264            return response()->json(['message' => $e->getMessage()], 422);
265        }
266    }
267}