Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
100.00% |
66 / 66 |
|
100.00% |
4 / 4 |
CRAP | |
100.00% |
1 / 1 |
| ReportFilterTrait | |
100.00% |
66 / 66 |
|
100.00% |
4 / 4 |
23 | |
100.00% |
1 / 1 |
| buildBaseMatchQuery | |
100.00% |
40 / 40 |
|
100.00% |
1 / 1 |
14 | |||
| toDailyUsageMatch | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| getDateRange | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
6 | |||
| resolveUserIds | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Traits; |
| 4 | |
| 5 | use App\Http\Models\Admin\CompanyGroup; |
| 6 | use App\Http\Models\Auth\Role; |
| 7 | use App\Http\Models\UserInfo; |
| 8 | use Carbon\Carbon; |
| 9 | use Illuminate\Http\Request; |
| 10 | use MongoDB\BSON\UTCDateTime; |
| 11 | |
| 12 | /** |
| 13 | * Shared filtering logic for report controllers. |
| 14 | * |
| 15 | * Provides methods to build MongoDB match queries scoped by company, group, |
| 16 | * subgroup, and user, respecting the caller's admin role hierarchy. |
| 17 | */ |
| 18 | trait ReportFilterTrait |
| 19 | { |
| 20 | /** |
| 21 | * Build a base MongoDB $match query scoped to the requesting user's |
| 22 | * permissions. The resulting array targets the UserInfo collection |
| 23 | * (fields: company_id, group_id, subgroup_id, user_id, status). |
| 24 | * |
| 25 | * @param Request $request The incoming HTTP request |
| 26 | * @return array MongoDB $match conditions |
| 27 | */ |
| 28 | protected function buildBaseMatchQuery(Request $request): array |
| 29 | { |
| 30 | $user = $request->user(); |
| 31 | $roles = $user->roles(); |
| 32 | $baseMatch = ['status' => ['$ne' => 'Invited']]; |
| 33 | $processIds = fn ($ids) => array_values(array_filter(explode(',', $ids ?? ''))); |
| 34 | $isCmc = $request->input('cmc', false); |
| 35 | |
| 36 | $companyIds = $processIds($request->input('company_ids', $request->input('companyIds'))); |
| 37 | $userIds = $processIds($request->input('user_ids', $request->input('userIds'))); |
| 38 | $rawGroupIds = $processIds($request->input('group_ids', $request->input('groupIds'))); |
| 39 | $rawSubgroupIds = $processIds($request->input('subgroup_ids', $request->input('subgroupIds'))); |
| 40 | $hasNotAssignedGroup = in_array('-1', $rawGroupIds); |
| 41 | $hasNotAssignedSubgroup = in_array('-1', $rawSubgroupIds); |
| 42 | $groupIds = array_filter($rawGroupIds, fn ($id) => $id !== '-1'); |
| 43 | $subgroupIds = array_filter($rawSubgroupIds, fn ($id) => $id !== '-1'); |
| 44 | |
| 45 | if (! $isCmc && empty($companyIds)) { |
| 46 | $companyIds = [$user->company_id]; |
| 47 | } |
| 48 | |
| 49 | if (! in_array(Role::VENGRESO_ADMIN, $roles)) { |
| 50 | $companyIds = [$user->company_id]; |
| 51 | if (! in_array(Role::GLOBAL_ADMIN, $roles)) { |
| 52 | $managedGroupIds = CompanyGroup::where('company_id', $user->company_id) |
| 53 | ->where('admins', $user->id) |
| 54 | ->pluck('id')->all(); |
| 55 | $groupIds = ! empty($groupIds) ? array_intersect($groupIds, $managedGroupIds) : $managedGroupIds; |
| 56 | } |
| 57 | } |
| 58 | |
| 59 | $orConditions = []; |
| 60 | |
| 61 | if (! $isCmc) { |
| 62 | $baseMatch['company_id'] = ['$in' => $companyIds]; |
| 63 | } elseif (! empty($companyIds)) { |
| 64 | $orConditions[] = ['company_id' => ['$in' => $companyIds]]; |
| 65 | } |
| 66 | |
| 67 | if (! empty($userIds)) { |
| 68 | $orConditions[] = ['user_id' => ['$in' => $userIds]]; |
| 69 | } |
| 70 | if (! empty($groupIds)) { |
| 71 | $orConditions[] = ['group_id' => ['$in' => $groupIds]]; |
| 72 | } |
| 73 | if ($hasNotAssignedGroup) { |
| 74 | $orConditions[] = ['group_id' => null]; |
| 75 | } |
| 76 | if (! empty($subgroupIds)) { |
| 77 | $orConditions[] = ['subgroup_id' => ['$in' => $subgroupIds]]; |
| 78 | } |
| 79 | if ($hasNotAssignedSubgroup) { |
| 80 | $orConditions[] = ['subgroup_id' => null]; |
| 81 | } |
| 82 | |
| 83 | if (! empty($orConditions)) { |
| 84 | $baseMatch['$or'] = $orConditions; |
| 85 | } |
| 86 | |
| 87 | return $baseMatch; |
| 88 | } |
| 89 | |
| 90 | /** |
| 91 | * Remap $baseMatch keys for FlyMsgUserDailyUsage queries. |
| 92 | * UserInfo uses 'status', but DailyUsage uses 'user_status'. |
| 93 | * |
| 94 | * @param array $baseMatch The base match query built from UserInfo fields |
| 95 | * @return array Remapped match query for FlyMsgUserDailyUsage |
| 96 | */ |
| 97 | protected function toDailyUsageMatch(array $baseMatch): array |
| 98 | { |
| 99 | if (isset($baseMatch['status'])) { |
| 100 | $baseMatch['user_status'] = $baseMatch['status']; |
| 101 | unset($baseMatch['status']); |
| 102 | } |
| 103 | |
| 104 | return $baseMatch; |
| 105 | } |
| 106 | |
| 107 | /** |
| 108 | * Resolve the date range for a report query. |
| 109 | * |
| 110 | * If 'from' and 'to' are provided in the request, those are used directly. |
| 111 | * Otherwise, the min/max dates are derived from the target collection. |
| 112 | * |
| 113 | * @param Request $request The incoming HTTP request |
| 114 | * @param array $baseMatch MongoDB $match conditions for the target collection |
| 115 | * @param string $model The Eloquent model class to query |
| 116 | * @param string $dateField The date field to aggregate on |
| 117 | * @return array [Carbon $startDate, Carbon $endDate] |
| 118 | */ |
| 119 | protected function getDateRange(Request $request, array $baseMatch, string $model, string $dateField = 'created_at'): array |
| 120 | { |
| 121 | if ($request->filled('from') && $request->filled('to')) { |
| 122 | return [Carbon::parse($request->from)->startOfDay(), Carbon::parse($request->to)->endOfDay()]; |
| 123 | } |
| 124 | $datePipeline = []; |
| 125 | if (! empty($baseMatch)) { |
| 126 | $datePipeline[] = ['$match' => $baseMatch]; |
| 127 | } |
| 128 | $datePipeline[] = ['$group' => ['_id' => null, 'minDate' => ['$min' => '$'.$dateField], 'maxDate' => ['$max' => '$'.$dateField]]]; |
| 129 | |
| 130 | $dateResult = $model::raw(fn ($c) => $c->aggregate($datePipeline))->first(); |
| 131 | |
| 132 | if ($dateResult && $dateResult->minDate) { |
| 133 | // Use Carbon::instance() to preserve full DateTime precision, then extend to |
| 134 | // day boundaries — matching the with-filter path and ensuring all records on |
| 135 | // the min/max days are included (getTimestamp()*1000 loses milliseconds and |
| 136 | // would exclude the most-recent record when it has a non-zero ms component). |
| 137 | $startDate = Carbon::instance($dateResult->minDate->toDateTime())->startOfDay(); |
| 138 | $endDate = Carbon::instance($dateResult->maxDate->toDateTime())->endOfDay(); |
| 139 | } else { |
| 140 | $startDate = Carbon::now()->subYear()->startOfDay(); |
| 141 | $endDate = Carbon::now()->endOfDay(); |
| 142 | } |
| 143 | |
| 144 | return [$startDate, $endDate]; |
| 145 | } |
| 146 | |
| 147 | /** |
| 148 | * Resolve scoped user IDs from UserInfo based on the base match query. |
| 149 | * |
| 150 | * @param array $baseMatch MongoDB $match conditions targeting UserInfo |
| 151 | * @return array List of user_id strings |
| 152 | */ |
| 153 | protected function resolveUserIds(array $baseMatch): array |
| 154 | { |
| 155 | $pipeline = [ |
| 156 | ['$match' => $baseMatch], |
| 157 | ['$project' => ['_id' => 0, 'user_id' => 1]], |
| 158 | ]; |
| 159 | |
| 160 | return collect(UserInfo::raw(fn ($c) => $c->aggregate($pipeline))) |
| 161 | ->pluck('user_id') |
| 162 | ->filter() |
| 163 | ->values() |
| 164 | ->all(); |
| 165 | } |
| 166 | } |