Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
44.60% covered (danger)
44.60%
186 / 417
59.09% covered (warning)
59.09%
13 / 22
CRAP
0.00% covered (danger)
0.00%
0 / 1
CompanyService
44.60% covered (danger)
44.60%
186 / 417
59.09% covered (warning)
59.09%
13 / 22
650.74
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
 updateCompany
0.00% covered (danger)
0.00%
0 / 57
0.00% covered (danger)
0.00%
0 / 1
306
 getCompanyBySlug
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getGroupBySlug
0.00% covered (danger)
0.00%
0 / 152
0.00% covered (danger)
0.00%
0 / 1
2
 addCompany
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 deactivateCompany
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
3.14
 deactivateCompanies
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 deleteCompanies
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 deleteCompany
85.71% covered (warning)
85.71%
18 / 21
0.00% covered (danger)
0.00%
0 / 1
8.19
 updateCompanyInstancy
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 createInstancyUser
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 createGroupInstancy
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 createCompany
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
 createLicenses
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
2.00
 handleSubscription
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
4.94
 processPocs
96.77% covered (success)
96.77%
30 / 31
0.00% covered (danger)
0.00%
0 / 1
3
 createNewUser
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 updateExistingUser
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 createPoc
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 sendInvitationMail
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
3
 createAdminUserInvitation
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
2
 createSubscription
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace App\Http\Services\Admin\Companies;
4
5use App\Events\User\Registered;
6use App\Helpers\DateHelper;
7use App\Helpers\FlyMSGLogger;
8use App\Http\Models\Admin\AdminUserInvitation;
9use App\Http\Models\Admin\Company;
10use App\Http\Models\Admin\CompanyGroup;
11use App\Http\Models\Admin\CompanyLicenses;
12use App\Http\Models\Auth\Role;
13use App\Http\Models\Auth\User;
14use App\Http\Models\Plans;
15use App\Http\Repositories\InstancyRepository;
16use App\Http\Services\Admin\Users\AdminUsersService;
17use App\Http\Services\InstancyServiceV2;
18use App\Http\Services\RoleplayAddonLifecycleService;
19use App\Mail\GlobalAdminInvitationExistentUserMail;
20use App\Mail\GlobalAdminInvitationMail;
21use App\Services\Email\EmailService;
22use Carbon\Carbon;
23use Illuminate\Support\Facades\DB;
24use Illuminate\Support\Str;
25use stdClass;
26
27class CompanyService
28{
29    protected $instancyRepository;
30
31    public function __construct(
32        InstancyRepository $instancyRepository,
33        private readonly AdminUsersService $adminUsersService,
34        private readonly EmailService $emailService
35    ) {
36        $this->instancyRepository = $instancyRepository;
37    }
38
39    public function updateCompany(Company $company, array $data): Company
40    {
41        $session = DB::getMongoClient()->startSession();
42        $session->startTransaction();
43
44        $previousRoleplayAddonId = $company->company_roleplay_addon_id;
45        $roleplayAddonKeyProvided = array_key_exists('company_roleplay_addon_id', $data);
46        $newRoleplayAddonId = $roleplayAddonKeyProvided
47            ? ($data['company_roleplay_addon_id'] ?: null)
48            : $previousRoleplayAddonId;
49
50        $updatePayload = [
51            'name' => $data['company_name'],
52            'slug' => Str::slug($data['company_name']),
53            'address_line_1' => $data['company_address_line1'],
54            'address_line_2' => $data['company_address_line2'],
55            'city' => $data['city'],
56            'state' => $data['state'],
57            'zip' => $data['zip_code'],
58            'country' => $data['country'],
59        ];
60
61        if ($roleplayAddonKeyProvided) {
62            $updatePayload['company_roleplay_addon_id'] = $newRoleplayAddonId;
63        }
64        if (array_key_exists('roleplay_addon_access', $data)) {
65            $updatePayload['roleplay_addon_access'] = $data['roleplay_addon_access'];
66        }
67
68        $company->update($updatePayload);
69
70        try {
71            $this->updateCompanyInstancy($company->instancy_id, $data['company_name']);
72        } catch (\Exception $e) {
73            FlyMSGLogger::logError(__METHOD__.' Update Company on Instancy', $e);
74        }
75
76        // / update company licenses
77        $termOfContract = $data['term_of_contract'] > 0 ? $data['term_of_contract'] : $data['custom_term_of_contract'];
78
79        $license = $company->licenses()->active()->first() ?? $company->licenses()->latest()->first();
80
81        $contractEndDate = Carbon::parse($data['contract_end_date']);
82
83        if (filled($data['extend_contract_end_date']) && $data['extend_contract_end_date'] > 0) {
84            $contractEndDate = $contractEndDate->addDays($data['extend_contract_end_date']);
85        }
86
87        // Update contract end date for all active users of the company
88        foreach ($company->users as $user) {
89            if (! empty($user->status) && strtolower($user->status) != 'active') {
90                continue;
91            }
92
93            $userSub = $user->subscriptions()->latest()->first();
94            if (isset($userSub->stripe_status) && $userSub->stripe_status == 'active') {
95                $userSub->ends_at = $contractEndDate->format('Y-m-d H:i:s');
96                $userSub->save();
97            }
98
99            // Update instancy membership details
100            $instancyService = new InstancyServiceV2;
101            $instancyService->updateMembership($user->email);
102        }
103
104        $license->update([
105            'term_of_contract' => DateHelper::getLabelByInterval($termOfContract),
106            'contract_start_date' => Carbon::parse($data['contract_start_date'])->format('Y-m-d H:i:s'),
107            'contract_end_date' => $contractEndDate->format('Y-m-d H:i:s'),
108            'business_pro_enterprise_plus' => $data['business_pro_enterprise_plus'],
109            'auto_renew_license' => $data['auto_renewal'],
110            'total_growth_license_count' => $data['growth'],
111            'total_starter_license_count' => $data['starter'],
112            'total_sales_pro_license_count' => $data['sales_pro'],
113            'total_sales_pro_teams_license_count' => $data['sales_pro_teams_smb'],
114        ]);
115
116        $session->commitTransaction();
117
118        // Fan-out corporate roleplay addon changes after the main write is
119        // durable. We intentionally run this outside the transaction: addon
120        // lifecycle writes are idempotent and one user-level failure must not
121        // roll back the whole company update.
122        if ($roleplayAddonKeyProvided && $previousRoleplayAddonId !== $newRoleplayAddonId) {
123            $lifecycle = app(RoleplayAddonLifecycleService::class);
124            if ($newRoleplayAddonId) {
125                $lifecycle->applyCompanyAddonToAllUsers($company->fresh(), $newRoleplayAddonId);
126            } else {
127                $lifecycle->cancelCompanyAddonForAllUsers($company->fresh());
128            }
129        }
130
131        return $company;
132    }
133
134    public function getCompanyBySlug(string $slug)
135    {
136        return Company::with(['licenses' => function ($query) {
137            $query->active();
138        }])->firstWhere('slug', $slug);
139    }
140
141    public function getGroupBySlug(string $company_id, string $slug)
142    {
143        $pipeline = [
144            [
145                '$match' => [
146                    'company_id' => $company_id,
147                    '$expr' => [
148                        '$eq' => [
149                            $slug,
150                            [
151                                '$toLower' => [
152                                    '$replaceAll' => [
153                                        'input' => '$name',
154                                        'find' => ' ',
155                                        'replacement' => '-',
156                                    ],
157                                ],
158                            ],
159                        ],
160                    ],
161                ],
162            ],
163            [
164                '$addFields' => [
165                    'converted_group_id' => ['$toString' => '$_id'],
166                ],
167            ],
168            [
169                '$lookup' => [
170                    'from' => 'company_groups',
171                    'localField' => 'converted_group_id',
172                    'foreignField' => 'parent_id',
173                    'as' => 'subgroups_data',
174                ],
175            ],
176            [
177                '$addFields' => [
178                    'all_group_and_subgroup_ids' => [
179                        '$concatArrays' => [
180                            ['$converted_group_id'],
181                            [
182                                '$map' => [
183                                    'input' => '$subgroups_data',
184                                    'as' => 's',
185                                    'in' => ['$toString' => '$$s._id'],
186                                ],
187                            ],
188                        ],
189                    ],
190                ],
191            ],
192            [
193                '$lookup' => [
194                    'from' => 'users',
195                    'localField' => 'all_group_and_subgroup_ids',
196                    'foreignField' => 'company_group_id',
197                    'as' => 'all_relevant_users',
198                ],
199            ],
200            [
201                '$addFields' => [
202                    'subgroups' => [
203                        '$map' => [
204                            'input' => '$subgroups_data',
205                            'as' => 'sub',
206                            'in' => [
207                                'id' => ['$toString' => '$$sub._id'],
208                                'name' => '$$sub.name',
209                                'slug' => [
210                                    '$toLower' => [
211                                        '$replaceAll' => [
212                                            'input' => '$$sub.name',
213                                            'find' => ' ',
214                                            'replacement' => '-',
215                                        ],
216                                    ],
217                                ],
218                                'users_count' => [
219                                    '$size' => [
220                                        '$filter' => [
221                                            'input' => '$all_relevant_users',
222                                            'as' => 'user',
223                                            'cond' => [
224                                                '$eq' => ['$$user.company_group_id', ['$toString' => '$$sub._id']],
225                                            ],
226                                        ],
227                                    ],
228                                ],
229                            ],
230                        ],
231                    ],
232                    'users_in_main_group' => [
233                        '$filter' => [
234                            'input' => '$all_relevant_users',
235                            'as' => 'user',
236                            'cond' => [
237                                '$and' => [
238                                    ['$eq' => ['$$user.company_group_id', '$converted_group_id']],
239                                    [
240                                        '$not' => [
241                                            '$in' => [
242                                                '$$user.company_group_id',
243                                                [
244                                                    '$map' => [
245                                                        'input' => '$subgroups_data',
246                                                        'as' => 's',
247                                                        'in' => ['$toString' => '$$s._id'],
248                                                    ],
249                                                ],
250                                            ],
251                                        ],
252                                    ],
253                                ],
254                            ],
255                        ],
256                    ],
257                ],
258            ],
259            [
260                '$project' => [
261                    '_id' => 0,
262                    'id' => '$_id',
263                    'name' => 1,
264                    'company_id' => 1,
265                    'slug' => [
266                        '$toLower' => [
267                            '$replaceAll' => [
268                                'input' => '$name',
269                                'find' => ' ',
270                                'replacement' => '-',
271                            ],
272                        ],
273                    ],
274                    'subgroups' => [
275                        '$concatArrays' => [
276                            '$subgroups',
277                            [
278                                [
279                                    'id' => null,
280                                    'name' => 'Not Assigned',
281                                    'slug' => 'not-assigned',
282                                    'users_count' => ['$size' => '$users_in_main_group'],
283                                ],
284                            ],
285                        ],
286                    ],
287                    'total_group_users_count' => ['$size' => '$all_relevant_users'],
288                ],
289            ],
290        ];
291
292        $results = CompanyGroup::raw(function ($collection) use ($pipeline) {
293            return $collection->aggregate($pipeline);
294        });
295
296        return $results->first();
297    }
298
299    public function addCompany($data): Company
300    {
301        $session = DB::getMongoClient()->startSession();
302        $session->startTransaction();
303        try {
304            $company = $this->createCompany($data['company']);
305            $companyLicense = $this->createLicenses($company, $data['company']);
306            $this->processPocs($company, $data['pocs'], $companyLicense);
307
308            $this->createInstancyUser($data['pocs'], $company->instancy_id);
309
310            $session->commitTransaction();
311
312            return $company;
313        } catch (\Exception $e) {
314            $session->abortTransaction();
315            FlyMSGLogger::logError(__METHOD__, $e);
316            throw $e;
317        }
318    }
319
320    public function deactivateCompany(Company $company, string $adminId, $cancellation_date = null)
321    {
322        // Not necessary to deactivate users when deactivating a company
323        $users = User::where('company_id', $company->id)->get();
324
325        foreach ($users as $user) {
326            $this->adminUsersService->deactivateUser($user, $adminId, $company->id, $cancellation_date);
327        }
328
329        $company->deactivated_at = Carbon::now();
330        $company->save();
331
332        // Strip corporate roleplay addons from every remaining user so they
333        // fall back to the freemium beginner plan.
334        try {
335            app(RoleplayAddonLifecycleService::class)->cancelCompanyAddonForAllUsers($company->fresh());
336        } catch (\Throwable $e) {
337            FlyMSGLogger::logError(__METHOD__.' cancelCompanyAddonForAllUsers', $e);
338        }
339    }
340
341    public function deactivateCompanies($companyIds, string $adminId)
342    {
343        $companies = Company::whereIn('_id', $companyIds)->get();
344
345        foreach ($companies as $company) {
346            $this->deactivateCompany($company, $adminId);
347        }
348    }
349
350    public function deleteCompanies(array $companyIds, User $admin)
351    {
352        if (in_array($admin->company_id, $companyIds)) {
353            throw new UnprocessableEntityHttpException('You cannot delete your own company.');
354        }
355
356        $companies = Company::whereIn('_id', $companyIds)->get();
357
358        foreach ($companies as $company) {
359            $this->deleteCompany($company, $admin);
360        }
361    }
362
363    public function deleteCompany(Company $company, User $admin)
364    {
365        if (isset($admin->company_id) && $admin->company_id === $company->id) {
366            throw new UnprocessableEntityHttpException('You cannot delete your own company.');
367        }
368
369        AdminUserInvitation::where('company_id', $company->id)->delete();
370        $company->licenses()->delete();
371        $company->pocs()->delete();
372        $company->groupsAndSubgroups()->delete();
373
374        foreach ($company->users as $user) {
375            $user->removeAllRoles();
376
377            $userSub = $user->subscription('main');
378            if ($userSub) {
379                if ($user->company_id) {
380                    if ($userSub->plan->identifier != Plans::FREEMIUM_IDENTIFIER) {
381                        $userSub->markAsCancelledOnlyInDB();
382                    }
383                } else {
384                    if ($userSub->plan->identifier != Plans::FREEMIUM_IDENTIFIER) {
385                        $userSub->cancel();
386                    }
387                }
388            }
389
390            $user->unset('company_id');
391            $user->unset('company_group_id');
392            $user->unset('invited_to_company');
393            $user->unset('invited_to_company_by_admin');
394            $user->save();
395        }
396
397        $company->delete();
398    }
399
400    private function updateCompanyInstancy($instancyId, $name)
401    {
402        $newInstancyGroup = new stdClass;
403        $newInstancyGroup->groupId = $instancyId;
404        $newInstancyGroup->name = $name;
405
406        return $this->instancyRepository->updateGroup($newInstancyGroup);
407    }
408
409    private function createInstancyUser(array $pocs, string $groupId)
410    {
411        collect($pocs)->map(function ($poc) use ($groupId) {
412            (new InstancyServiceV2)->createInstancyUser($poc['email'], $groupId);
413        });
414    }
415
416    private function createGroupInstancy($companyName)
417    {
418        $newInstancyGroup = new stdClass;
419        $newInstancyGroup->name = $companyName;
420
421        return $this->instancyRepository->createGroup($newInstancyGroup);
422    }
423
424    private function createCompany(array $companyData): Company
425    {
426        $instancyId = $this->createGroupInstancy($companyData['company_name']);
427
428        return Company::create([
429            'name' => $companyData['company_name'],
430            'slug' => Str::slug($companyData['company_name']),
431            'address_line_1' => $companyData['company_address_line1'],
432            'address_line_2' => $companyData['company_address_line2'],
433            'city' => $companyData['city'],
434            'state' => $companyData['state'],
435            'zip' => $companyData['zip_code'],
436            'country' => $companyData['country'],
437            'instancy_id' => $instancyId,
438        ]);
439    }
440
441    private function createLicenses(Company $company, array $companyData): CompanyLicenses
442    {
443        $termOfContract = $companyData['term_of_contract'] > 0
444            ? $companyData['term_of_contract']
445            : $companyData['custom_term_of_contract'];
446
447        $companyLicenseCreated = $company->licenses()->create([
448            'term_of_contract' => DateHelper::getLabelByInterval($termOfContract),
449            'contract_start_date' => Carbon::parse($companyData['contract_start_date'])->format('Y-m-d H:i:s'),
450            'contract_end_date' => Carbon::parse($companyData['contract_end_date'])->format('Y-m-d H:i:s'),
451            'auto_renew_license' => $companyData['auto_renewal'],
452            'business_pro_enterprise_plus' => $companyData['business_pro_enterprise_plus'],
453            'total_growth_license_count' => $companyData['growth'],
454            'total_starter_license_count' => $companyData['starter'],
455            'total_sales_pro_license_count' => $companyData['sales_pro'],
456            'total_growth_license_remaining' => $companyData['growth'],
457            'total_starter_license_remaining' => $companyData['starter'],
458            'total_sales_pro_license_remaining' => $companyData['sales_pro'],
459            'total_sales_pro_teams_license_count' => $companyData['sales_pro_teams_smb'],
460            'total_sales_pro_teams_license_remaining' => $companyData['sales_pro_teams_smb'],
461        ]);
462
463        return $companyLicenseCreated;
464    }
465
466    private function handleSubscription(User $user, string $contractEndDate, string $stripeId, ?User $existingUser): void
467    {
468        if ($existingUser) {
469            $userSub = $user->subscription('main');
470            if ($userSub) {
471                $userSub->cancel();
472            }
473        }
474
475        $this->createSubscription($contractEndDate, $stripeId, $user);
476    }
477
478    private function processPocs(Company $company, array $pocs, CompanyLicenses $companyLicense): void
479    {
480        $admin = auth()->user();
481
482        collect($pocs)->map(function ($poc, $index) use ($company, $companyLicense, $admin) {
483            $existingUser = User::firstWhere('email', $poc['email']);
484            $password = Str::password(16);
485            $hashed_password = bcrypt($password);
486            $tempPasswordExpiry = Carbon::now()->addDays(7);
487
488            $data = [
489                'email' => $poc['email'],
490                'first_name' => $poc['first_name'],
491                'last_name' => $poc['last_name'],
492                'company_id' => $company->id,
493                'company_group_id' => null,
494                'company_subgroup_id' => null,
495                'temp_password_expiry' => $tempPasswordExpiry,
496                'temp_password' => $hashed_password,
497                'password' => $hashed_password,
498                'admin_email' => $admin->email,
499                'email_verified_at' => Carbon::now(),
500                'invited_to_company' => true,
501                'invited_to_company_by_admin' => $admin->email,
502            ];
503            $user = $existingUser ?? $this->createNewUser($data);
504
505            $plan = Plans::find($poc['plan_id']);
506            $stripeId = $plan->stripe_id;
507
508            $this->handleSubscription($user, $companyLicense->contract_end_date, $stripeId, $existingUser);
509
510            if ($existingUser) {
511                $this->updateExistingUser($existingUser, $data);
512            }
513
514            $this->createPoc($company, $poc, $user->id, $index === 0 ? true : false);
515            $this->createAdminUserInvitation($user, $hashed_password, $plan);
516            $this->sendInvitationMail($poc['email'], $user, filled($existingUser), $password);
517        });
518    }
519
520    private function createNewUser(array $poc): User
521    {
522        $user = new User;
523        $user->fill($poc);
524        $user->selected_plan = 'Sales Pro Teams';
525        $user->status = 'Invited';
526        $user->save();
527        $user->assignRole(Role::GLOBAL_ADMIN, []);
528        $user->onboardingv2_presented = true;
529
530        Registered::dispatch($user, $poc);
531
532        return $user;
533    }
534
535    private function updateExistingUser(User $existingUser, array $userData): void
536    {
537        $existingUser->update([
538            'company_id' => $userData['company_id'],
539            'company_group_id' => null,
540            'status' => 'Invited',
541            'invited_to_company' => true,
542            'invited_to_company_by_admin' => $userData['invited_to_company_by_admin'],
543        ]);
544        $existingUser->assignRole(Role::GLOBAL_ADMIN, []);
545    }
546
547    private function createPoc(Company $company, array $poc, string $userId, bool $isRoot): void
548    {
549        $company->pocs()->create([
550            'email' => $poc['email'],
551            'first_name' => $poc['first_name'],
552            'last_name' => $poc['last_name'],
553            'user_id' => $userId,
554            'root' => $isRoot,
555        ]);
556    }
557
558    public function sendInvitationMail(string $email, User $user, bool $isExistingUser, string $password): void
559    {
560        $inviter = auth()->user()->email;
561        $tempPasswordExpiry = Carbon::now()->addDays(7);
562
563        try {
564            if ($isExistingUser) {
565                $this->emailService->send(
566                    $email,
567                    new GlobalAdminInvitationExistentUserMail(
568                        $email,
569                        $user->first_name,
570                        $user->last_name,
571                        $inviter,
572                        $password,
573                        $tempPasswordExpiry->format('m/d/Y').' at '.$tempPasswordExpiry->format('h:i A'),
574                        $user->avatar ?? ''
575                    ),
576                    'cac_invite_existing_user'
577                );
578            } else {
579                $this->emailService->send(
580                    $email,
581                    new GlobalAdminInvitationMail(
582                        $email,
583                        $user->first_name,
584                        $user->last_name,
585                        $inviter,
586                        $password,
587                        $tempPasswordExpiry->format('m/d/Y').' at '.$tempPasswordExpiry->format('h:i A')
588                    ),
589                    'cac_invite_user'
590                );
591            }
592        } catch (\Exception $e) {
593            FlyMSGLogger::logError(__METHOD__, $e);
594        }
595    }
596
597    private function createAdminUserInvitation(User $user, string $hashed_password, ?Plans $plan = null): void
598    {
599        $data = [
600            'role_name' => Role::GLOBAL_ADMIN,
601            'email' => $user->email,
602            'company_id' => $user->company_id,
603            'company_group_id' => $user->group_id,
604            'company_subgroup_id' => $user->subgroup_id,
605            'temp_password_expiry' => $user->temp_password_expiry,
606            'temp_password' => $hashed_password,
607            'password' => $hashed_password,
608            'admin_email' => auth()->user()->email,
609        ];
610
611        if ($plan) {
612            $data['plan'] = $plan;
613            $data['plan_id'] = $plan->id;
614        }
615
616        AdminUserInvitation::updateOrCreate(['email' => $user->email], $data);
617    }
618
619    private function createSubscription($endDate, string $stripeId, User $user)
620    {
621        $user->subscriptions()->create([
622            'name' => 'main',
623            'stripe_status' => 'active',
624            'stripe_plan' => $stripeId,
625            'user_id' => $user->id,
626            'quantity' => '1',
627            'ends_at' => $endDate,
628            'starts_at' => Carbon::now()->toDateTimeString(),
629        ]);
630    }
631}