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