Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.51% covered (success)
98.51%
66 / 67
80.00% covered (warning)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
SubscriptionRepository
98.51% covered (success)
98.51%
66 / 67
80.00% covered (warning)
80.00%
4 / 5
12
0.00% covered (danger)
0.00%
0 / 1
 getActiveSubscription
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 isSubscriptionValid
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
4.02
 getFreemiumPlan
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
1
 getPlanByIdentifier
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getActiveSubscriptionCountsByPlan
100.00% covered (success)
100.00%
32 / 32
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3namespace App\Http\Repositories;
4
5use App\Http\Models\Auth\User;
6use App\Http\Models\Plans;
7use App\Http\Models\Subscription;
8use App\Http\Repositories\interfaces\ISubscriptionRepository;
9use Carbon\Carbon;
10use Laravel\Cashier\Cashier;
11use MongoDB\BSON\UTCDateTime;
12use Stripe\Subscription as StripeSubscription;
13
14/**
15 * Repository for subscription data access operations.
16 *
17 * This repository handles all database queries related to subscriptions,
18 * keeping data access logic separate from business logic.
19 */
20class SubscriptionRepository implements ISubscriptionRepository
21{
22    /**
23     * Get the active subscription for a user.
24     *
25     * This method fixes the bug where the original subscription() method
26     * could return a deactivated subscription. It queries all subscriptions
27     * ordered by created_at DESC and returns the first valid one.
28     *
29     * @param  User  $user  The user to get the subscription for
30     * @param  string  $name  The subscription name (default: 'main')
31     * @return Subscription|null The active subscription or null if none found
32     */
33    public function getActiveSubscription(User $user, string $name = 'main'): ?Subscription
34    {
35        $subscriptions = Subscription::where('user_id', $user->getKey())
36            ->where('name', $name)
37            ->orderBy('created_at', 'desc')
38            ->get();
39
40        foreach ($subscriptions as $subscription) {
41            if ($this->isSubscriptionValid($subscription)) {
42                return $subscription;
43            }
44        }
45
46        return null;
47    }
48
49    /**
50     * Check if a subscription is valid considering team plan specifics.
51     */
52    private function isSubscriptionValid(Subscription $subscription): bool
53    {
54        if (! $subscription->valid() || ! filled($subscription->plan)) {
55            return false;
56        }
57
58        $isTeamUser = in_array($subscription->plan->identifier, [
59            Plans::ProPlanTeamsSMB,
60            Plans::ProPlanTeamsENT,
61        ]);
62
63        if ($isTeamUser) {
64            return ! $subscription->proTeamEnded();
65        }
66
67        return true;
68    }
69
70    /**
71     * Get the freemium plan.
72     *
73     * @return Plans The freemium plan
74     */
75    public function getFreemiumPlan(): Plans
76    {
77        return Plans::select(
78            'title',
79            'identifier',
80            'features',
81            'currency',
82            'interval',
83            'unit_amount',
84            'user_persona_available',
85            'user_custom_prompts',
86            'has_fly_learning',
87            'regenerate_count',
88            'flygrammar_actions',
89            'flycut_deployment',
90            'prompts_per_day',
91            'can_disable_flygrammar',
92            'flycuts_features'
93        )->firstWhere('identifier', Plans::FREEMIUM_IDENTIFIER);
94    }
95
96    /**
97     * Get a plan by its identifier.
98     *
99     * @param  string  $identifier  The plan identifier
100     * @return Plans|null The plan or null if not found
101     */
102    public function getPlanByIdentifier(string $identifier): ?Plans
103    {
104        return Plans::firstWhere('identifier', $identifier);
105    }
106
107    /**
108     * Get counts of active subscriptions grouped by stripe_plan.
109     *
110     * Uses the same active subscription criteria as Subscription::scopeActive():
111     * - ends_at is null OR ends_at > now (grace period)
112     * - stripe_status NOT IN [incomplete, incomplete_expired, unpaid]
113     * - If Cashier::$deactivatePastDue is true: excludes past_due status
114     *
115     * @return array<string, int> Map of stripe_plan to active subscription count
116     */
117    public function getActiveSubscriptionCountsByPlan(): array
118    {
119        // Convert to MongoDB UTCDateTime for proper BSON date comparison
120        $now = new UTCDateTime(Carbon::now()->getTimestampMs());
121
122        // Build the list of excluded statuses (matching Subscription::scopeActive)
123        $excludedStatuses = [
124            StripeSubscription::STATUS_INCOMPLETE,
125            StripeSubscription::STATUS_INCOMPLETE_EXPIRED,
126            StripeSubscription::STATUS_UNPAID,
127        ];
128
129        if (Cashier::$deactivatePastDue) {
130            $excludedStatuses[] = StripeSubscription::STATUS_PAST_DUE;
131        }
132
133        // Use MongoDB aggregation pipeline
134        $results = Subscription::raw(function ($collection) use ($now, $excludedStatuses) {
135            return $collection->aggregate([
136                // Match active subscriptions
137                [
138                    '$match' => [
139                        'stripe_plan' => ['$ne' => null],
140                        'stripe_status' => ['$nin' => $excludedStatuses],
141                        '$or' => [
142                            ['ends_at' => null],
143                            ['ends_at' => ['$gt' => $now]],
144                        ],
145                    ],
146                ],
147                // Group by stripe_plan and count
148                [
149                    '$group' => [
150                        '_id' => '$stripe_plan',
151                        'count' => ['$sum' => 1],
152                    ],
153                ],
154            ]);
155        });
156
157        // Convert to array map
158        $counts = [];
159        foreach ($results as $result) {
160            $counts[$result->_id] = (int) $result->count;
161        }
162
163        return $counts;
164    }
165}