Passed
Push — developer ( 3a394b...2c7c5e )
by Radosław
14:40
created

AutoAssign::getModuleName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
/**
3
 * Auto Assign file.
4
 *
5
 * The file is part of the paid functionality. Using the file is allowed only after purchasing a subscription.
6
 * File modification allowed only with the consent of the system producer.
7
 *
8
 * @package App
9
 *
10
 * @copyright YetiForce S.A.
11
 * @license   YetiForce Public License 5.0 (licenses/LicenseEN.txt or yetiforce.com)
12
 * @author    Radosław Skrzypczak <[email protected]>
13
 * @author    Mariusz Krzaczkowski <[email protected]>
14
 */
15
16
namespace App;
17
18
/**
19
 * Auto Assign class.
20
 */
21
class AutoAssign extends Base
22
{
23
	/** @var string Basic table name */
24
	public const TABLE_NAME = 's_#__auto_assign';
25
	/** @var array Members tables */
26
	public const MEMBERS_TABLES = ['s_#__auto_assign_users' => 'id', 's_#__auto_assign_groups' => 'id', 's_#__auto_assign_roles' => 'id'];
27
	/** @var string Round robin table name */
28
	public const ROUND_ROBIN_TABLE = 'u_#__auto_assign_rr';
29
30
	/** @var int Status inactive */
31
	public const STATUS_INACTIVE = 0;
32
	/** @var int Status active */
33
	public const STATUS_ACTIVE = 1;
34
35
	/** @var int Manual mode */
36
	public const MODE_MANUAL = 1;
37
	/** @var int Handler mode */
38
	public const MODE_HANDLER = 2;
39
	/** @var int Workflow mode */
40
	public const MODE_WORKFLOW = 4;
41
42
	/** @var int Load balance method */
43
	public const METHOD_LOAD_BALANCE = 0;
44
	/** @var int Round robin method */
45
	public const METHOD_ROUND_ROBIN = 1;
46
47
	/**
48
	 * Get all auto assign entries for module.
49
	 *
50
	 * @param string $moduleName
51
	 * @param int    $mode       A bitmask of one or more of the mode flags
52
	 * @param int    $state
53
	 *
54
	 * @return array
55
	 */
56
	public static function getByModule(string $moduleName, int $mode = self::MODE_HANDLER | self::MODE_WORKFLOW | self::MODE_MANUAL, int $state = self::STATUS_ACTIVE): array
57
	{
58
		$query = (new Db\Query())->from(self::TABLE_NAME)
59
			->where(['tabid' => Module::getModuleId($moduleName), 'state' => $state]);
60
		$mods = ['or'];
61
		foreach ([self::MODE_MANUAL => 'gui', self::MODE_HANDLER => 'handler', self::MODE_WORKFLOW => 'workflow'] as $key => $column) {
62
			if ($mode & $key) {
63
				$mods[] = [$column => 1];
64
			}
65
		}
66
		$query->andWhere($mods);
67
68
		return $query->all();
69
	}
70
71
	/**
72
	 * Get all auto assign instances for module.
73
	 *
74
	 * @param string   $moduleName
75
	 * @param int|null $mode       A bitmask of one or more of the mode flags
76
	 *
77
	 * @return array
78
	 */
79
	public static function getInstancesByModule(string $moduleName, int $mode = null): array
80
	{
81
		$instances = [];
82
		foreach (self::getByModule($moduleName, $mode) as $autoAssignData) {
83
			$instances[$autoAssignData['id']] = self::getInstance($autoAssignData);
84
		}
85
		return $instances;
86
	}
87
88
	/**
89
	 * Get auto assign instance for record.
90
	 *
91
	 * @param \Vtiger_Record_Model $recordModel
92
	 * @param int|null             $mode        A bitmask of one or more of the mode flags
93
	 *
94
	 * @return self|null
95
	 */
96
	public static function getAutoAssignForRecord(\Vtiger_Record_Model $recordModel, int $mode = null): ?self
97
	{
98
		$autoAssignInstance = null;
99
		foreach (self::getByModule($recordModel->getModuleName(), $mode) as $autoAssignData) {
100
			$conditions = Json::isEmpty($autoAssignData['conditions']) ? [] : Json::decode($autoAssignData['conditions']);
101
			if (Condition::checkConditions($conditions, $recordModel)) {
102
				$autoAssignInstance = self::getInstance($autoAssignData);
103
				break;
104
			}
105
		}
106
		return $autoAssignInstance;
107
	}
108
109
	/**
110
	 * Get auto assign instance by ID.
111
	 *
112
	 * @param int $id
113
	 *
114
	 * @return self|null
115
	 */
116
	public static function getInstanceById(int $id): ?self
117
	{
118
		$data = (new Db\Query())->from(self::TABLE_NAME)->where(['id' => $id])->one();
119
		return $data ? (new self())->setData($data) : null;
120
	}
121
122
	/**
123
	 * Get auto assign instance by data.
124
	 *
125
	 * @param array $data
126
	 *
127
	 * @return self|null
128
	 */
129
	public static function getInstance(array $data): ?self
130
	{
131
		return $data ? (new self())->setData($data) : null;
132
	}
133
134
	/**
135
	 * Function to get the Id.
136
	 *
137
	 * @return int
138
	 */
139
	public function getId(): int
140
	{
141
		return $this->get('id');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->get('id') could return the type null which is incompatible with the type-hinted return integer. Consider adding an additional type-check to rule them out.
Loading history...
142
	}
143
144
	/**
145
	 * Get name of auto assign instance.
146
	 *
147
	 * @param bool $encode
148
	 *
149
	 * @return string
150
	 */
151
	public function getName(bool $encode = true): string
152
	{
153
		return Language::translate($this->get('subject'), 'Settings:AutomaticAssignment', false, $encode);
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type null|string expected by parameter $language of App\Language::translate(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

153
		return Language::translate($this->get('subject'), 'Settings:AutomaticAssignment', /** @scrutinizer ignore-type */ false, $encode);
Loading history...
154
	}
155
156
	/**
157
	 * Get module name.
158
	 *
159
	 * @return string
160
	 */
161
	public function getModuleName(): string
162
	{
163
		return Module::getModuleName($this->get('tabid'));
0 ignored issues
show
Bug Best Practice introduced by
The expression return App\Module::getMo...me($this->get('tabid')) could return the type boolean which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
164
	}
165
166
	/**
167
	 * Check conditions for record.
168
	 *
169
	 * @param \Vtiger_Record_Model $recordModel
170
	 *
171
	 * @return bool
172
	 */
173
	public function checkConditionForRecord(\Vtiger_Record_Model $recordModel): bool
174
	{
175
		$conditions = Json::isEmpty($this->get('conditions')) ? [] : Json::decode($this->get('conditions'));
176
		return Condition::checkConditions($conditions, $recordModel);
177
	}
178
179
	/**
180
	 * Check if the instance is active in a given mode.
181
	 *
182
	 * @param int $mode
183
	 *
184
	 * @return bool
185
	 */
186
	public function isActive(int $mode): bool
187
	{
188
		switch ($mode) {
189
			case self::MODE_MANUAL:
190
				$result = !$this->isEmpty('gui');
191
				break;
192
			case self::MODE_HANDLER:
193
				$result = !$this->isEmpty('handler');
194
				break;
195
			case self::MODE_WORKFLOW:
196
				$result = !$this->isEmpty('workflow');
197
				break;
198
			default:
199
				$result = false;
200
				break;
201
		}
202
		return $result && self::STATUS_ACTIVE === (int) $this->get('state');
203
	}
204
205
	/**
206
	 * Get an automatic selected user ID.
207
	 *
208
	 * @return int|null
209
	 */
210
	public function getOwner(): ?int
211
	{
212
		switch ($this->get('method')) {
213
			case self::METHOD_LOAD_BALANCE:
214
				$owner = $this->getQueryByLoadBalance()->scalar() ?: null;
215
				break;
216
			case self::METHOD_ROUND_ROBIN:
217
				$owner = $this->getQueryByRoundRobin()->scalar() ?: null;
218
				break;
219
			default:
220
				$owner = null;
221
				break;
222
		}
223
224
		return $owner ? $owner : $this->getDefaultOwner();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $owner ? $owner : $this->getDefaultOwner() could return the type string which is incompatible with the type-hinted return integer|null. Consider adding an additional type-check to rule them out.
Loading history...
225
	}
226
227
	/**
228
	 * Get automatic selected users.
229
	 *
230
	 * @return array
231
	 */
232
	public function getOwners(): array
233
	{
234
		switch ($this->get('method')) {
235
			case self::METHOD_LOAD_BALANCE:
236
				$owner = $this->getQueryByLoadBalance()->all();
237
				break;
238
			case self::METHOD_ROUND_ROBIN:
239
				$owner = $this->getQueryByRoundRobin()->all();
240
				break;
241
			default:
242
				$owner = [];
243
				break;
244
		}
245
246
		return $owner;
247
	}
248
249
	/**
250
	 * Get default owner.
251
	 *
252
	 * @return int
253
	 */
254
	public function getDefaultOwner(): ?int
255
	{
256
		$owner = null;
257
		$defaultOwner = (int) $this->get('default_assign');
258
		$ownerModel = Fields\Owner::getInstance($this->getModuleName());
259
260
		$type = $defaultOwner ? Fields\Owner::getType($defaultOwner) : null;
261
		if ('Users' === $type) {
0 ignored issues
show
introduced by
The condition 'Users' === $type is always false.
Loading history...
262
			$owner = User::isExists($defaultOwner) ? $defaultOwner : $owner;
263
		} elseif ($type) {
264
			$owner = \array_key_exists($defaultOwner, $ownerModel->getAccessibleGroupForModule()) ? $defaultOwner : $owner;
265
		}
266
267
		return $owner;
268
	}
269
270
	/**
271
	 * Query object for users allowed to be assigned by load balanced method.
272
	 *
273
	 * In order to correctly balance the entries attribution
274
	 * we need ot randomize the order in which they are returned.
275
	 * Otherwise, when multiple users have the same amount of entries
276
	 * it is always the first one in the results who will be assigned to new entry.
277
	 *
278
	 * @return Db\Query
279
	 */
280
	public function getQueryByLoadBalance(): Db\Query
281
	{
282
		return $this->getQuery()->orderBy(['count' => SORT_ASC, new \yii\db\Expression('RAND()')]);
283
	}
284
285
	/**
286
	 * Query object for users allowed to be assigned by round robin.
287
	 *
288
	 * @return Db\Query
289
	 */
290
	public function getQueryByRoundRobin(): Db\Query
291
	{
292
		$robinTable = self::ROUND_ROBIN_TABLE;
293
		$columnName = "{$robinTable}.datetime";
294
		$id = $this->getId();
295
296
		return $this->getQuery()->leftJoin($robinTable, "vtiger_users.id = {$robinTable}.user AND {$robinTable}.id={$id}")
297
			->addSelect([$columnName])
298
			->addGroupBy($columnName)
299
			->orderBy([$columnName => SORT_ASC]);
300
	}
301
302
	/**
303
	 * Query object for users allowed for assignment.
304
	 *
305
	 * @return Db\Query
306
	 */
307
	public function getQuery(): Db\Query
308
	{
309
		$ownerFieldName = 'assigned_user_id';
310
		$queryGeneratorUsers = $this->getAvailableUsersQuery();
311
312
		$queryGenerator = (new QueryGenerator($this->getModuleName()));
313
		$queryGenerator->permissions = false;
314
		$conditions = Json::isEmpty($this->get('record_limit_conditions')) ? [] : Json::decode($this->get('record_limit_conditions'));
315
		$queryGenerator->setFields([$ownerFieldName])
316
			->setCustomColumn(['count' => new \yii\db\Expression('COUNT(*)')])
317
			->setConditions($conditions)
318
			->setGroup($ownerFieldName)
319
			->addNativeCondition([$queryGenerator->getColumnName($ownerFieldName) => $queryGeneratorUsers->createQuery()]);
320
		$subQuery = $queryGenerator->createQuery();
321
322
		$recordLimit = (int) $this->get('record_limit');
323
		if (0 === $recordLimit) {
324
			$queryGeneratorUsers->setCustomColumn(['temp_limit' => $queryGeneratorUsers->getColumnName('records_limit')]);
325
		} else {
326
			$queryGeneratorUsers->setCustomColumn(['temp_limit' => new \yii\db\Expression($recordLimit)]);
327
		}
328
		$queryGeneratorUsers->setGroup('id')->setCustomGroup(['temp_limit', 'count']);
329
		$query = $queryGeneratorUsers->createQuery(true);
330
		$query->leftJoin(['crm_data_temp_table' => $subQuery], "crm_data_temp_table.{$ownerFieldName}={$queryGeneratorUsers->getColumnName('id')}");
331
		$query->addSelect(['crm_data_temp_table.count']);
332
		$query->andHaving(['or', ['<', 'count', new \yii\db\Expression('temp_limit')], ['temp_limit' => 0], ['count' => null]]);
333
334
		return $query;
335
	}
336
337
	/**
338
	 * Query generator object of available users.
339
	 *
340
	 * @return QueryGenerator
341
	 */
342
	public function getAvailableUsersQuery(): QueryGenerator
343
	{
344
		$queryGenerator = (new QueryGenerator('Users'))
345
			->setFields(['id'])
346
			->addCondition('status', 'Active', 'e')
347
			->addCondition('available', 1, 'e')
348
			->addCondition('auto_assign', 1, 'e');
349
		if ($this->get('working_hours')) {
350
			$currentTime = date('H:i');
351
			$queryGenerator->addNativeCondition(['<=', $queryGenerator->getColumnName('start_hour'), $currentTime])
352
				->addNativeCondition(['>',  $queryGenerator->getColumnName('end_hour'), $currentTime]);
353
		}
354
		$columnName = $queryGenerator->getColumnName('id');
355
356
		$condition = ['or'];
357
		foreach ($this->getMembers() as $member) {
358
			[$type, $id] = explode(':', $member);
359
			switch ($type) {
360
				case PrivilegeUtil::MEMBER_TYPE_USERS:
361
					$condition[$type][$columnName][] = (int) $id;
362
					break;
363
				case PrivilegeUtil::MEMBER_TYPE_GROUPS:
364
					$condition[] = [$columnName => (new Db\Query())->select(['userid'])->from(["condition_{$type}_{$id}_" . Layout::getUniqueId() => PrivilegeUtil::getQueryToUsersByGroup((int) $id)])];
365
					break;
366
				case PrivilegeUtil::MEMBER_TYPE_ROLES:
367
					$condition[] = [$columnName => PrivilegeUtil::getQueryToUsersByRole($id)];
368
					break;
369
				case PrivilegeUtil::MEMBER_TYPE_ROLE_AND_SUBORDINATES:
370
					$condition[] = [$columnName => PrivilegeUtil::getQueryToUsersByRoleAndSubordinate($id)];
371
					break;
372
				default:
373
					break;
374
			}
375
		}
376
		if (1 === \count($condition)) {
377
			$condition = [$columnName => 0];
378
		}
379
		$queryGenerator->addNativeCondition($condition);
380
381
		return $queryGenerator;
382
	}
383
384
	/**
385
	 * Get members.
386
	 *
387
	 * @return array
388
	 */
389
	public function getMembers(): array
390
	{
391
		if (!$this->has('members')) {
392
			$queryAll = null;
393
			foreach (self::MEMBERS_TABLES as $tableName => $index) {
394
				$query = (new Db\Query())
395
					->select(['member' => new \yii\db\Expression('CONCAT(type,\':\', member)')])
396
					->from($tableName)
397
					->where(["{$tableName}.{$index}" => $this->getId()]);
398
				if ($queryAll) {
399
					$queryAll->union($query, true);
400
				} else {
401
					$queryAll = $query;
402
				}
403
			}
404
			$members = $queryAll->column();
0 ignored issues
show
Bug introduced by
The method column() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

404
			/** @scrutinizer ignore-call */ 
405
   $members = $queryAll->column();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
405
			$this->set('members', $members);
406
		}
407
		return $this->get('members');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->get('members') could return the type null which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
408
	}
409
410
	/**
411
	 * Post process action.
412
	 *
413
	 * @param int $userId
414
	 *
415
	 * @return void
416
	 */
417
	public function postProcess(int $userId)
418
	{
419
		$dbCommand = Db::getInstance()->createCommand();
420
		if ($userId && self::METHOD_ROUND_ROBIN === (int) $this->get('method')) {
421
			$params = ['id' => $this->getId(), 'user' => $userId];
422
			$isExists = (new Db\Query())->from(self::ROUND_ROBIN_TABLE)->where($params)->exists();
423
			if ($isExists) {
424
				$dbCommand->update(self::ROUND_ROBIN_TABLE, ['datetime' => (new \DateTime())->format('Y-m-d H:i:s.u')], $params)->execute();
425
			} elseif (\App\User::isExists($userId, false)) {
426
				$params['datetime'] = (new \DateTime())->format('Y-m-d H:i:s.u');
427
				$dbCommand->insert(self::ROUND_ROBIN_TABLE, $params)->execute();
428
			}
429
		}
430
	}
431
}
432