Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 386
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
CompanyLicensesService
0.00% covered (danger)
0.00%
0 / 386
0.00% covered (danger)
0.00%
0 / 10
462
0.00% covered (danger)
0.00%
0 / 1
 getAllCompaniesLicenses
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
12
 getCompanyLicenses
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 assignPlan
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
12
 validatePlansAvailability
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
30
 getCompaniesLicenses
0.00% covered (danger)
0.00%
0 / 204
0.00% covered (danger)
0.00%
0 / 1
6
 getCompanyLicensePlanDetails
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
12
 getPlanDetailsMapping
0.00% covered (danger)
0.00%
0 / 82
0.00% covered (danger)
0.00%
0 / 1
2
 calculateLicensesUsed
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 reduceAvailableLicenses
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getCompaniesLicensesInvitations
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace App\Http\Services\Admin\Companies;
4
5use App\Exceptions\ExpectedException;
6use App\Http\Models\Admin\AdminUserInvitation;
7use App\Http\Models\Admin\Company;
8use App\Http\Models\Admin\CompanyLicenses;
9use App\Http\Models\Auth\User;
10use App\Http\Models\Plans;
11use App\Traits\CompanyTrait;
12use Carbon\Carbon;
13use MongoDB\BSON\ObjectId;
14
15class CompanyLicensesService
16{
17    use CompanyTrait;
18
19    public function getAllCompaniesLicenses()
20    {
21        $companiesLicenses = $this->getCompaniesLicenses(null);
22        $allPlans = Plans::whereNull('pricing_version')->get();
23
24        $result = [];
25        foreach ($companiesLicenses as $companyLicenses) {
26            $companyInvitationLicenses = $this->getCompaniesLicensesInvitations($companyLicenses->company_id);
27            // return $companyInvitationLicenses[$planId] ?? 0;
28            $plans = ['starter', 'growth', 'sales_pro', 'sales_pro_teams'];
29
30            $plans = array_filter($plans, function ($plan) use ($companyLicenses) {
31                $totalLicensesField = "total_{$plan}_license_count";
32
33                return $companyLicenses->$totalLicensesField > 0;
34            });
35
36            $plansUsage = collect($plans)->map(function ($plan) use ($companyLicenses, $allPlans, $companyInvitationLicenses) {
37                return $this->getCompanyLicensePlanDetails($plan, $companyLicenses, $allPlans, $companyInvitationLicenses, true);
38            });
39
40            $totalLicensesAvailable = max(0, collect($plansUsage)->sum('licensesAvailable'));
41
42            // $plansUsage[] = [
43            //     "value" => Plans::FREEMIUM_IDENTIFIER,
44            //     "label" => "Freemium",
45            //     "licensesAvailable" => 'Unlimited',
46            //     "stripe_id" => null,
47            // ];
48
49            $result[] = [
50                'id' => $companyLicenses->company_id,
51                'name' => $companyLicenses->name,
52                'slug' => $companyLicenses->slug,
53                'licensesAvailable' => $totalLicensesAvailable,
54                'plans' => array_values($plansUsage->toArray()),
55            ];
56        }
57
58        usort($result, function ($a, $b) {
59            if ($a['licensesAvailable'] === $b['licensesAvailable']) {
60                return $a['name'] <=> $b['name'];
61            }
62
63            return $b['licensesAvailable'] <=> $a['licensesAvailable'];
64        });
65
66        return $result;
67    }
68
69    /**
70     * @return array<array{
71     *     value: string,
72     *     label: string,
73     *     licensesAvailable: int|string,
74     *     totalLicenses: int|string
75     *     stripe_id: string
76     * }>
77     */
78    public function getCompanyLicenses(string $companyId): array
79    {
80        $result = $this->getCompaniesLicenses(null);
81        $allPlans = Plans::whereNull('pricing_version')->get();
82        $companyInvitationLicenses = $this->getCompaniesLicensesInvitations($companyId);
83
84        $companyLicenses = $result->firstWhere('company_id', $companyId);
85
86        $plans = ['starter', 'growth', 'sales_pro', 'sales_pro_teams'];
87
88        $plans = array_filter($plans, function ($plan) use ($companyLicenses) {
89            $totalLicensesField = "total_{$plan}_license_count";
90
91            return $companyLicenses->$totalLicensesField > 0;
92        });
93
94        return collect($plans)->map(function ($plan) use ($companyLicenses, $allPlans, $companyInvitationLicenses) {
95            return $this->getCompanyLicensePlanDetails($plan, $companyLicenses, $allPlans, $companyInvitationLicenses);
96        })->toArray();
97    }
98
99    public function assignPlan(CompanyLicenses $companyLicense, Plans $plan, User $user)
100    {
101        $this->reduceAvailableLicenses($companyLicense, $plan);
102
103        if ($user->subscribed('main')) {
104            $user->subscription('main')->cancel();
105        } else {
106            $subscription = $user->subscriptions()->latest()->first();
107            if ($subscription) {
108                $subscription->update([
109                    'stripe_status' => 'canceled',
110                    'ends_at' => Carbon::now()->toDateTimeString(),
111                ]);
112            }
113        }
114
115        $end_date = $companyLicense->contract_end_date;
116
117        $user->subscriptions()->create([
118            'name' => 'main',
119            'stripe_status' => 'active',
120            'stripe_plan' => $plan->stripe_id,
121            'quantity' => '1',
122            'ends_at' => $end_date,
123            'starts_at' => Carbon::now()->toDateTimeString(),
124        ]);
125
126        // need to update hubspot properties
127    }
128
129    /**
130     * Validate plans availability.
131     *
132     * @param  array<array{email: string, plan: string}>  $request
133     * @param  string  $CompanyId
134     * @return void
135     */
136    public function validatePlansAvailability(array $request, string $companyId)
137    {
138        $licenses = $this->getCompanyLicenses($companyId);
139
140        if (empty($licenses)) {
141            throw new ExpectedException("The company doesn't have any active license.");
142        }
143
144        $allPlans = Plans::whereNull('pricing_version')->get();
145        $plansRequestCount = array_count_values(array_column($request['users'], 'plan'));
146        $planDetails = $this->getPlanDetailsMapping($allPlans);
147
148        foreach ($plansRequestCount as $planId => $count) {
149            $plan = Plans::where('stripe_id', $planId)->first();
150            $planIdentifier = str_replace('-', '_', $plan->identifier);
151            $planCmc = $planDetails[$planIdentifier];
152
153            if (! $planCmc) {
154                throw new ExpectedException("The plan $plan->title was not found for this company.");
155            }
156
157            $planAvailableLicenses = array_filter($licenses, function ($license) use ($planCmc) {
158                return $license['stripe_id'] === $planCmc['stripe_id'];
159            });
160
161            $planAvailableLicenses = array_shift($planAvailableLicenses);
162
163            if ($planAvailableLicenses['licensesAvailable'] < $count) {
164                throw new ExpectedException("Failed to assign $plan->title. The company doesn't have enough licenses.");
165            }
166        }
167    }
168
169    private function getCompaniesLicenses(?string $companyId)
170    {
171        $queryMatch = [
172            'deactivated_at' => ['$exists' => false],
173            'deleted_at' => ['$exists' => false],
174        ];
175
176        if ($companyId) {
177            $queryMatch[] = [
178                '_id' => new ObjectId($companyId),
179            ];
180        }
181
182        $query = [
183            [
184                '$match' => $queryMatch,
185            ],
186            [
187                '$lookup' => [
188                    'from' => 'company_licenses',
189                    'let' => [
190                        'companyId' => [
191                            '$toString' => '$_id',
192                        ],
193                    ],
194                    'pipeline' => [
195                        [
196                            '$match' => [
197                                '$expr' => [
198                                    '$eq' => ['$company_id', '$$companyId'],
199                                ],
200                            ],
201                        ],
202                        [
203                            '$project' => [
204                                'total_sales_pro_teams_license_count' => 1,
205                                'total_sales_pro_license_count' => 1,
206                                'total_growth_license_count' => 1,
207                                'total_starter_license_count' => 1,
208                                'purchased_licenses' => [
209                                    '$add' => [
210                                        '$total_sales_pro_teams_license_count',
211                                        '$total_sales_pro_license_count',
212                                        '$total_growth_license_count',
213                                        '$total_starter_license_count',
214                                    ],
215                                ],
216                            ],
217                        ],
218                    ],
219                    'as' => 'licenses_data',
220                ],
221            ],
222            [
223                '$unwind' => [
224                    'path' => '$licenses_data',
225                    'preserveNullAndEmptyArrays' => true,
226                ],
227            ],
228            [
229                '$lookup' => [
230                    'from' => 'users',
231                    'let' => [
232                        'companyId' => [
233                            '$toString' => '$_id',
234                        ],
235                    ],
236                    'pipeline' => [
237                        [
238                            '$match' => [
239                                '$expr' => [
240                                    '$eq' => ['$company_id', '$$companyId'],
241                                ],
242                                'status' => 'Active',
243                            ],
244                        ],
245                        [
246                            '$lookup' => [
247                                'from' => 'subscriptions',
248                                'let' => [
249                                    'userId' => [
250                                        '$toString' => '$_id',
251                                    ],
252                                    'userStatus' => '$status',
253                                ],
254                                'pipeline' => [
255                                    [
256                                        '$match' => [
257                                            '$expr' => [
258                                                '$and' => [
259                                                    ['$eq' => ['$user_id', '$$userId']],
260                                                    [
261                                                        '$or' => [
262                                                            ['$eq' => ['$name', 'main']],
263                                                            ['$eq' => ['$name', 'invitation']],
264                                                        ],
265                                                    ],
266                                                    ['$eq' => ['$stripe_status', 'active']],
267                                                    ['$ne' => ['$$userStatus', 'Deactivated']],
268                                                ],
269                                            ],
270                                        ],
271                                    ],
272                                    [
273                                        '$sort' => [
274                                            'created_at' => -1,
275                                        ],
276                                    ],
277                                    [
278                                        '$limit' => 1,
279                                    ],
280                                    [
281                                        '$lookup' => [
282                                            'from' => 'plans',
283                                            'localField' => 'stripe_plan',
284                                            'foreignField' => 'stripe_id',
285                                            'as' => 'plan_data',
286                                        ],
287                                    ],
288                                    [
289                                        '$unwind' => [
290                                            'path' => '$plan_data',
291                                            'preserveNullAndEmptyArrays' => true,
292                                        ],
293                                    ],
294                                    [
295                                        '$group' => [
296                                            '_id' => '$plan_data.identifier',
297                                            'subscription_count' => [
298                                                '$sum' => 1,
299                                            ],
300                                        ],
301                                    ],
302                                    [
303                                        '$project' => [
304                                            '_id' => 0,
305                                            'identifier' => '$_id',
306                                            'count' => '$subscription_count',
307                                        ],
308                                    ],
309                                ],
310                                'as' => 'users_subscriptions_data',
311                            ],
312                        ],
313                        [
314                            '$unwind' => [
315                                'path' => '$users_subscriptions_data',
316                                'preserveNullAndEmptyArrays' => true,
317                            ],
318                        ],
319                        [
320                            '$group' => [
321                                '_id' => '$users_subscriptions_data.identifier',
322                                'total_count' => [
323                                    '$sum' => '$users_subscriptions_data.count',
324                                ],
325                            ],
326                        ],
327                        [
328                            '$project' => [
329                                '_id' => 0,
330                                'identifier' => '$_id',
331                                'count' => '$total_count',
332                            ],
333                        ],
334                        [
335                            '$group' => [
336                                '_id' => null,
337                                'subscriptions_usage' => [
338                                    '$push' => [
339                                        'identifier' => '$identifier',
340                                        'count' => '$count',
341                                    ],
342                                ],
343                            ],
344                        ],
345                        [
346                            '$project' => [
347                                '_id' => 0,
348                                'subscriptions_usage' => 1,
349                            ],
350                        ],
351                    ],
352                    'as' => 'sub_data',
353                ],
354            ],
355            [
356                '$unwind' => [
357                    'path' => '$sub_data',
358                    'preserveNullAndEmptyArrays' => true,
359                ],
360            ],
361            [
362                '$project' => [
363                    'company_id' => ['$toString' => '$_id'],
364                    'name' => '$name',
365                    'slug' => '$slug',
366                    'purchased_licenses' => '$licenses_data.purchased_licenses',
367                    'total_sales_pro_teams_license_count' => '$licenses_data.total_sales_pro_teams_license_count',
368                    'total_sales_pro_license_count' => '$licenses_data.total_sales_pro_license_count',
369                    'total_growth_license_count' => '$licenses_data.total_growth_license_count',
370                    'total_starter_license_count' => '$licenses_data.total_starter_license_count',
371                    'subscriptions_usage' => '$sub_data.subscriptions_usage',
372                ],
373            ],
374        ];
375
376        return Company::raw(function ($collection) use ($query) {
377            return $collection->aggregate($query);
378        });
379    }
380
381    private function getCompanyLicensePlanDetails(string $identifier, $companyLicenses, $allPlans, $companyInvitationLicenses, $withIdentifier = false)
382    {
383        $planDetails = $this->getPlanDetailsMapping($allPlans);
384
385        if (! isset($planDetails[$identifier])) {
386            return null;
387        }
388
389        $plan = $planDetails[$identifier];
390
391        $invitationLicenseUsage = $companyInvitationLicenses[$plan['plan_id']] ?? 0;
392        $licensesUsed = $this->calculateLicensesUsed($plan['identifiers'], $companyLicenses->subscriptions_usage) + $invitationLicenseUsage;
393        $licensesAvailable = $companyLicenses->{$plan['totalLicensesField']} - $licensesUsed;
394
395        return [
396            'value' => $withIdentifier ? $plan['value'] : $plan['label'],
397            'label' => $plan['label'],
398            'licensesAvailable' => $licensesAvailable,
399            'totalLicenses' => $companyLicenses->{$plan['totalLicensesField']},
400            'stripe_id' => $plan['stripe_id'],
401        ];
402    }
403
404    private function getPlanDetailsMapping($allPlans): array
405    {
406        return [
407            'starter' => [
408                'value' => Plans::STARTER_MONTHLY_IDENTIFIER,
409                'label' => 'Starter',
410                'stripe_id' => $allPlans->firstWhere('identifier', Plans::STARTER_YEARLY_IDENTIFIER)?->stripe_id ?? null,
411                'plan_id' => $allPlans->firstWhere('identifier', Plans::STARTER_YEARLY_IDENTIFIER)?->id ?? null,
412                'identifiers' => ['starter', 'starter-yearly'],
413                'totalLicensesField' => 'total_starter_license_count',
414            ],
415            'starter_yearly' => [
416                'value' => Plans::STARTER_YEARLY_IDENTIFIER,
417                'label' => 'Starter',
418                'stripe_id' => $allPlans->firstWhere('identifier', Plans::STARTER_YEARLY_IDENTIFIER)?->stripe_id ?? null,
419                'plan_id' => $allPlans->firstWhere('identifier', Plans::STARTER_YEARLY_IDENTIFIER)?->id ?? null,
420                'identifiers' => ['starter', 'starter-yearly'],
421                'totalLicensesField' => 'total_starter_license_count',
422            ],
423            'growth' => [
424                'value' => Plans::GROWTH_MONTHLY_IDENTIFIER,
425                'label' => 'Growth',
426                'stripe_id' => $allPlans->firstWhere('identifier', Plans::GROWTH_YEARLY_IDENTIFIER)?->stripe_id ?? null,
427                'plan_id' => $allPlans->firstWhere('identifier', Plans::GROWTH_YEARLY_IDENTIFIER)?->id ?? null,
428                'identifiers' => ['growth', 'growth-yearly'],
429                'totalLicensesField' => 'total_growth_license_count',
430            ],
431            'growth_yearly' => [
432                'value' => Plans::GROWTH_YEARLY_IDENTIFIER,
433                'label' => 'Growth',
434                'stripe_id' => $allPlans->firstWhere('identifier', Plans::GROWTH_YEARLY_IDENTIFIER)?->stripe_id ?? null,
435                'plan_id' => $allPlans->firstWhere('identifier', Plans::GROWTH_YEARLY_IDENTIFIER)?->id ?? null,
436                'identifiers' => ['growth', 'growth-yearly'],
437                'totalLicensesField' => 'total_growth_license_count',
438            ],
439            'sales_pro' => [
440                'value' => Plans::PROFESSIONAL_YEARLY_IDENTIFIER,
441                'label' => 'Sales Pro',
442                'stripe_id' => $allPlans->firstWhere('identifier', Plans::PROFESSIONAL_YEARLY_IDENTIFIER)?->stripe_id ?? null,
443                'plan_id' => $allPlans->firstWhere('identifier', Plans::PROFESSIONAL_YEARLY_IDENTIFIER)?->id ?? null,
444                'identifiers' => ['sales-pro', 'sales-pro-yearly', 'sales-pro-monthly'],
445                'totalLicensesField' => 'total_sales_pro_license_count',
446            ],
447            'sales_pro_yearly' => [
448                'value' => Plans::PROFESSIONAL_YEARLY_IDENTIFIER,
449                'label' => 'Sales Pro',
450                'stripe_id' => $allPlans->firstWhere('identifier', Plans::PROFESSIONAL_YEARLY_IDENTIFIER)?->stripe_id ?? null,
451                'plan_id' => $allPlans->firstWhere('identifier', Plans::PROFESSIONAL_YEARLY_IDENTIFIER)?->id ?? null,
452                'identifiers' => ['sales-pro', 'sales-pro-yearly', 'sales-pro-monthly'],
453                'totalLicensesField' => 'total_sales_pro_license_count',
454            ],
455            'sales_pro_monthly' => [
456                'value' => Plans::PROFESSIONAL_MONTHLY_IDENTIFIER,
457                'label' => 'Sales Pro',
458                'stripe_id' => $allPlans->firstWhere('identifier', Plans::PROFESSIONAL_YEARLY_IDENTIFIER)?->stripe_id ?? null,
459                'plan_id' => $allPlans->firstWhere('identifier', Plans::PROFESSIONAL_YEARLY_IDENTIFIER)?->id ?? null,
460                'identifiers' => ['sales-pro', 'sales-pro-yearly', 'sales-pro-monthly'],
461                'totalLicensesField' => 'total_sales_pro_license_count',
462            ],
463            'sales_pro_teams' => [
464                'value' => Plans::ProPlanTeamsENT,
465                'label' => 'Sales Pro Teams',
466                'stripe_id' => $allPlans->firstWhere('identifier', Plans::ProPlanTeamsENT)?->stripe_id ?? null,
467                'plan_id' => $allPlans->firstWhere('identifier', Plans::ProPlanTeamsENT)?->id ?? null,
468                'identifiers' => ['sales-pro-teams', 'pro-plan-teams-smb', 'pro-plan-teams-ent'],
469                'totalLicensesField' => 'total_sales_pro_teams_license_count',
470            ],
471            'pro_plan_teams_smb' => [
472                'value' => Plans::ProPlanTeamsSMB,
473                'label' => 'Sales Pro Teams',
474                'stripe_id' => $allPlans->firstWhere('identifier', Plans::ProPlanTeamsENT)?->stripe_id ?? null,
475                'plan_id' => $allPlans->firstWhere('identifier', Plans::ProPlanTeamsENT)?->id ?? null,
476                'identifiers' => ['sales-pro-teams', 'pro-plan-teams-smb', 'pro-plan-teams-ent'],
477                'totalLicensesField' => 'total_sales_pro_teams_license_count',
478            ],
479            'pro_plan_teams_ent' => [
480                'value' => Plans::ProPlanTeamsENT,
481                'label' => 'Sales Pro Teams',
482                'stripe_id' => $allPlans->firstWhere('identifier', Plans::ProPlanTeamsENT)?->stripe_id ?? null,
483                'plan_id' => $allPlans->firstWhere('identifier', Plans::ProPlanTeamsENT)?->id ?? null,
484                'identifiers' => ['sales-pro-teams', 'pro-plan-teams-smb', 'pro-plan-teams-ent'],
485                'totalLicensesField' => 'total_sales_pro_teams_license_count',
486            ],
487        ];
488    }
489
490    private function calculateLicensesUsed(array $identifiers, $subscriptionsUsage): int
491    {
492        return collect($identifiers)
493            ->sum(
494                fn ($id) => collect($subscriptionsUsage)
495                    ->firstWhere('identifier', $id)['count'] ?? 0
496            );
497    }
498
499    private function reduceAvailableLicenses(CompanyLicenses $companyLicense, Plans $plan)
500    {
501        $licensePropertyName = $this->getPlanLicenseProperty($plan);
502
503        $companyLicense->decrement($licensePropertyName);
504    }
505
506    private function getCompaniesLicensesInvitations(string $companyId)
507    {
508        $invitations = AdminUserInvitation::where('company_id', $companyId)->get();
509
510        return $invitations->groupBy('plan_id')->map(function ($group) {
511            return $group->count();
512        });
513    }
514}