Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 386 |
|
0.00% |
0 / 10 |
CRAP | |
0.00% |
0 / 1 |
CompanyLicensesService | |
0.00% |
0 / 386 |
|
0.00% |
0 / 10 |
462 | |
0.00% |
0 / 1 |
getAllCompaniesLicenses | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
12 | |||
getCompanyLicenses | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
2 | |||
assignPlan | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
12 | |||
validatePlansAvailability | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
30 | |||
getCompaniesLicenses | |
0.00% |
0 / 204 |
|
0.00% |
0 / 1 |
6 | |||
getCompanyLicensePlanDetails | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
12 | |||
getPlanDetailsMapping | |
0.00% |
0 / 82 |
|
0.00% |
0 / 1 |
2 | |||
calculateLicensesUsed | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
reduceAvailableLicenses | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getCompaniesLicensesInvitations | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace App\Http\Services\Admin\Companies; |
4 | |
5 | use App\Exceptions\ExpectedException; |
6 | use App\Http\Models\Admin\AdminUserInvitation; |
7 | use App\Http\Models\Admin\Company; |
8 | use App\Http\Models\Admin\CompanyLicenses; |
9 | use App\Http\Models\Auth\User; |
10 | use App\Http\Models\Plans; |
11 | use App\Traits\CompanyTrait; |
12 | use Carbon\Carbon; |
13 | use MongoDB\BSON\ObjectId; |
14 | |
15 | class 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 | } |