Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
66 / 66
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
ReportFilterTrait
100.00% covered (success)
100.00%
66 / 66
100.00% covered (success)
100.00%
4 / 4
23
100.00% covered (success)
100.00%
1 / 1
 buildBaseMatchQuery
100.00% covered (success)
100.00%
40 / 40
100.00% covered (success)
100.00%
1 / 1
14
 toDailyUsageMatch
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getDateRange
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
6
 resolveUserIds
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace App\Traits;
4
5use App\Http\Models\Admin\CompanyGroup;
6use App\Http\Models\Auth\Role;
7use App\Http\Models\UserInfo;
8use Carbon\Carbon;
9use Illuminate\Http\Request;
10use 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 */
18trait 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}