TransactionService::findCurrentOne()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 4
c 0
b 0
f 0
nc 2
nop 2
dl 0
loc 7
rs 10
1
<?php
2
3
namespace app\core\services;
4
5
use app\core\exceptions\CannotOperateException;
6
use app\core\exceptions\InternalException;
7
use app\core\exceptions\InvalidArgumentException;
8
use app\core\helpers\ArrayHelper;
9
use app\core\models\Account;
10
use app\core\models\Category;
11
use app\core\models\Record;
12
use app\core\models\Tag;
13
use app\core\models\Transaction;
14
use app\core\traits\ServiceTrait;
15
use app\core\types\DirectionType;
16
use app\core\types\RecordSource;
17
use app\core\types\TransactionType;
18
use Exception;
19
use Yii;
20
use yii\base\BaseObject;
21
use yii\base\InvalidConfigException;
22
use yii\db\Exception as DBException;
23
use yii\db\Expression;
24
use yii\web\NotFoundHttpException;
25
use yiier\graylog\Log;
26
use yiier\helpers\Setup;
27
28
/**
29
 * @property-read int $accountIdByDesc
30
 */
31
class TransactionService extends BaseObject
32
{
33
    use ServiceTrait;
34
35
    /**
36
     * @param Transaction $transaction
37
     * @return bool
38
     * @throws \yii\db\Exception
39
     */
40
    public static function createUpdateRecord(Transaction $transaction)
41
    {
42
        $data = [];
43
        if (in_array($transaction->type, [TransactionType::EXPENSE, TransactionType::TRANSFER])) {
44
            array_push($data, ['direction' => DirectionType::EXPENSE, 'account_id' => $transaction->from_account_id]);
45
        }
46
        if (in_array($transaction->type, [TransactionType::INCOME, TransactionType::TRANSFER])) {
47
            array_push($data, ['direction' => DirectionType::INCOME, 'account_id' => $transaction->to_account_id]);
48
        }
49
        $model = new Record();
50
        foreach ($data as $datum) {
51
            $conditions = ['transaction_id' => $transaction->id, 'direction' => $datum['direction']];
52
            if (!$_model = Record::find()->where($conditions)->one()) {
53
                $_model = clone $model;
54
                $_model->source = $transaction->source;
0 ignored issues
show
Documentation Bug introduced by
The property $source was declared of type integer, but $transaction->source is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
55
            }
56
            $_model->user_id = $transaction->user_id;
57
            $_model->transaction_id = $transaction->id;
58
            $_model->category_id = $transaction->category_id;
59
            $_model->amount_cent = $transaction->amount_cent;
60
            $_model->currency_amount_cent = $transaction->currency_amount_cent;
61
            $_model->currency_code = $transaction->currency_code;
62
            $_model->date = $transaction->date;
63
            $_model->exclude_from_stats = $transaction->exclude_from_stats;
0 ignored issues
show
Documentation Bug introduced by
The property $exclude_from_stats was declared of type integer, but $transaction->exclude_from_stats is of type boolean. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
64
            $_model->transaction_type = $transaction->type;
65
            $_model->load($datum, '');
66
            if (!$_model->save()) {
67
                throw new DBException(Setup::errorMessage($_model->firstErrors));
68
            }
69
        }
70
        return true;
71
    }
72
73
    public function createByCSV($filename)
74
    {
75
        ini_set("memory_limit", "1024M");
76
        ini_set("set_time_limit", "0");
77
        ini_set('max_execution_time', 1200); //1200 seconds = 20 minutes
78
        $filename = $this->uploadService->getFullFilename($filename);
79
        $row = $total = $success = $fail = 0;
80
        $items = [];
81
        $model = new Transaction();
82
        if (($handle = fopen($filename, "r")) !== false) {
0 ignored issues
show
Bug introduced by
It seems like $filename can also be of type false; however, parameter $filename of fopen() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

82
        if (($handle = fopen(/** @scrutinizer ignore-type */ $filename, "r")) !== false) {
Loading history...
83
            while (($data = fgetcsv($handle)) !== false) {
84
                $row++;
85
                // 去除第一行数据
86
                if ($row <= 1) {
87
                    continue;
88
                }
89
90
                $num = count($data);
91
                $newData = [];
92
                for ($c = 0; $c < $num; $c++) {
93
                    $newData[$c] = trim($data[$c]);
94
                }
95
                $_model = clone $model;
96
                try {
97
                    // 账单日期,类别,收入/支出,金额(CNY),标签(多个英文逗号隔开),描述,备注,账户1,账户2
98
                    //2020-08-20,餐饮食品,支出,28.9,,买菜28.9,,
99
                    $baseConditions = ['user_id' => Yii::$app->user->id];
100
                    $_model->date = Yii::$app->formatter->asDatetime(strtotime($newData[0]), 'php:Y-m-d H:i');
101
                    $_model->category_id = Category::find()->where($baseConditions + ['name' => $newData[1]])->scalar();
0 ignored issues
show
Documentation Bug introduced by
It seems like app\core\models\Category...$newData[1]))->scalar() can also be of type false or string. However, the property $category_id is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
102
                    if (!$_model->category_id) {
103
                        throw new DBException(Yii::t('app', 'Category not found.'));
104
                    }
105
                    $accountId = Account::find()->where($baseConditions + ['name' => $newData[7]])->scalar();
106
                    $accountId = $accountId ?: data_get(AccountService::getDefaultAccount(), 'id');
107
                    if (!$accountId) {
108
                        throw new DBException(Yii::t('app', 'Default account not found.'));
109
                    }
110
                    switch ($newData[2]) {
111
                        case '收入':
112
                            $_model->type = TransactionType::getName(TransactionType::INCOME);
0 ignored issues
show
Documentation Bug introduced by
The property $type was declared of type integer, but app\core\types\Transacti...ransactionType::INCOME) is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
113
                            $_model->to_account_id = $accountId;
0 ignored issues
show
Documentation Bug introduced by
It seems like $accountId can also be of type string. However, the property $to_account_id is declared as type integer|null. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
114
                            break;
115
                        case '支出':
116
                            $_model->type = TransactionType::getName(TransactionType::EXPENSE);
117
                            $_model->from_account_id = $accountId;
0 ignored issues
show
Documentation Bug introduced by
It seems like $accountId can also be of type string. However, the property $from_account_id is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
118
                            break;
119
                        case '转账':
120
                            $_model->type = TransactionType::getName(TransactionType::TRANSFER);
121
                            $_model->from_account_id = $accountId;
122
                            $_model->to_account_id = Account::find()
123
                                ->where($baseConditions + ['name' => $newData[8]])
124
                                ->scalar();
125
                            if (!$_model->to_account_id) {
126
                                throw new InvalidArgumentException($newData[8] . '转账「账户2」不能为空');
127
                            }
128
                            break;
129
                        default:
130
                            # code...
131
                            break;
132
                    }
133
                    $_model->currency_amount = abs($newData[3]);
134
                    $_model->currency_code = 'CNY';
135
                    $_model->tags = array_filter(explode('/', $newData[4]));
136
                    $_model->description = $newData[5];
137
                    $_model->remark = $newData[6];
138
139
                    $_model->source = RecordSource::IMPORT;
140
                    if (!$_model->validate()) {
141
                        throw new DBException(Setup::errorMessage($_model->firstErrors));
142
                    }
143
                    array_push($items, $_model);
144
                } catch (\Exception $e) {
145
                    Log::error('导入运费失败', [$newData, (string)$e]);
146
                    $failList[] = [
147
                        'data' => $newData,
148
                        'reason' => $e->getMessage(),
149
                    ];
150
                    $fail++;
151
                }
152
                $total++;
153
            }
154
            fclose($handle);
155
156
            if (!$fail) {
157
                /** @var Transaction $item */
158
                foreach (array_reverse($items) as $item) {
159
                    $item->save();
160
                    $success++;
161
                }
162
            }
163
164
            return [
165
                'total' => $total,
166
                'success' => $success,
167
                'fail' => $fail,
168
                'fail_list' => $failList ?? []
169
            ];
170
        }
171
    }
172
173
    /**
174
     * @param Transaction $transaction
175
     * @param array $changedAttributes
176
     * @throws Exception|\Throwable
177
     */
178
    public static function deleteRecord(Transaction $transaction, array $changedAttributes)
179
    {
180
        $type = $transaction->type;
181
        if (data_get($changedAttributes, 'type') && $transaction->type !== TransactionType::TRANSFER) {
182
            $direction = $type == TransactionType::INCOME ? DirectionType::EXPENSE : DirectionType::INCOME;
183
            Record::deleteAll(['transaction_id' => $transaction->id, 'direction' => $direction]);
184
        }
185
    }
186
187
    /**
188
     * @param int $id
189
     * @param int $userId
190
     * @return Transaction
191
     * @throws NotFoundHttpException
192
     * @throws Exception
193
     */
194
    public function copy(int $id, int $userId = 0)
195
    {
196
        $model = $this->findCurrentOne($id, $userId);
197
        $transaction = new Transaction();
198
        $values = $model->toArray();
199
        unset($values['date']);
200
        $transaction->date = Yii::$app->formatter->asDatetime('now', 'php:Y-m-d H:i');
201
        $transaction->source = RecordSource::CRONTAB;
202
        $transaction->load($values, '');
203
        if (!$transaction->save(false)) {
204
            throw new Exception(Setup::errorMessage($transaction->firstErrors));
205
        }
206
        return $transaction;
207
    }
208
209
    /**
210
     * @param string $desc
211
     * @param null|int $source
212
     * @return Transaction
213
     * @throws InternalException
214
     * @throws \Throwable
215
     */
216
    public function createByDesc(string $desc, $source = null): Transaction
217
    {
218
        $model = new Transaction();
219
        try {
220
            $model->description = $desc;
221
            $model->user_id = Yii::$app->user->id;
0 ignored issues
show
Documentation Bug introduced by
It seems like Yii::app->user->id can also be of type string. However, the property $user_id is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
222
            $rules = $this->getRuleService()->getRulesByDesc($desc);
223
            $model->type = $this->getDataByDesc(
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getDataByDesc($ru...ion(...) { /* ... */ }) can also be of type string. However, the property $type is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
224
                $rules,
225
                'then_transaction_type',
226
                function () use ($desc) {
227
                    if (ArrayHelper::strPosArr($desc, ['收到', '收入']) !== false) {
228
                        return TransactionType::getName(TransactionType::INCOME);
229
                    }
230
                    return TransactionType::getName(TransactionType::EXPENSE);
231
                }
232
            );
233
            $transactionType = TransactionType::toEnumValue($model->type);
0 ignored issues
show
Bug introduced by
It seems like $model->type can also be of type null; however, parameter $v of app\core\types\BaseType::toEnumValue() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

233
            $transactionType = TransactionType::toEnumValue(/** @scrutinizer ignore-type */ $model->type);
Loading history...
234
235
            if (in_array($transactionType, [TransactionType::EXPENSE, TransactionType::TRANSFER])) {
236
                $model->from_account_id = $this->getDataByDesc(
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getDataByDesc($ru... 'getAccountIdByDesc')) can also be of type string. However, the property $from_account_id is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
237
                    $rules,
238
                    'then_from_account_id',
239
                    [$this, 'getAccountIdByDesc']
240
                );
241
                if (!$model->from_account_id) {
242
                    throw new CannotOperateException(Yii::t('app', 'Default account not found.'));
243
                }
244
            }
245
246
            if (in_array($transactionType, [TransactionType::INCOME, TransactionType::TRANSFER])) {
247
                $model->to_account_id = $this->getDataByDesc(
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getDataByDesc($ru... 'getAccountIdByDesc')) can also be of type string. However, the property $to_account_id is declared as type integer|null. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
248
                    $rules,
249
                    'then_to_account_id',
250
                    [$this, 'getAccountIdByDesc']
251
                );
252
                if (!$model->to_account_id) {
253
                    throw new CannotOperateException(Yii::t('app', 'Default account not found.'));
254
                }
255
            }
256
257
            $model->category_id = $this->getDataByDesc(
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getDataByDesc($ru...ion(...) { /* ... */ }) can also be of type string. However, the property $category_id is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
258
                $rules,
259
                'then_category_id',
260
                function () {
261
                    //  todo 根据交易类型查找默认分类
262
                    return (int)data_get(CategoryService::getDefaultCategory(), 'id', 0);
263
                }
264
            );
265
266
            $model->date = $this->getDateByDesc($desc);
267
268
            $model->tags = $this->getDataByDesc($rules, 'then_tags');
269
            $model->status = $this->getDataByDesc($rules, 'then_transaction_status');
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getDataByDesc($ru...en_transaction_status') can also be of type string. However, the property $status is declared as type integer|null. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
270
            $model->reimbursement_status = $this->getDataByDesc($rules, 'then_reimbursement_status');
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getDataByDesc($ru..._reimbursement_status') can also be of type string. However, the property $reimbursement_status is declared as type integer|null. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
271
272
            $model->currency_amount = $this->getAmountByDesc($desc);
273
            $model->currency_code = user('base_currency_code');
274
            if (!$model->save()) {
275
                throw new DBException(Setup::errorMessage($model->firstErrors));
276
            }
277
            $source ? Record::updateAll(['source' => $source], ['transaction_id' => $model->id]) : null;
278
            return $model;
279
        } catch (Exception $e) {
280
            Yii::error(
281
                ['request_id' => Yii::$app->requestId->id, $model->attributes, $model->errors, (string)$e],
282
                __FUNCTION__
283
            );
284
            throw new InternalException($e->getMessage());
285
        }
286
    }
287
288
    /**
289
     * @param Record[] $records
290
     * @return array
291
     * @throws InvalidConfigException
292
     */
293
    public function formatRecords(array $records)
294
    {
295
        $items = [];
296
        foreach ($records as $record) {
297
            $key = Yii::$app->formatter->asDatetime(strtotime($record->date), 'php:Y-m-d');
298
            $items[$key]['records'][] = $record;
299
            $items[$key]['date'] = $key;
300
            $types = [TransactionType::EXPENSE, TransactionType::INCOME];
301
            if (in_array($record->transaction_type, $types)) {
302
                // todo 计算有待优化
303
                if ($record->direction === DirectionType::EXPENSE) {
304
                    $items[$key]['record_out_amount_cent'][] = $record->amount_cent;
305
                    $items[$key]['out'] = Setup::toYuan(array_sum($items[$key]['record_out_amount_cent']));
306
                }
307
                if ($record->direction === DirectionType::INCOME) {
308
                    $items[$key]['record_in_amount_cent'][] = $record->amount_cent;
309
                    $items[$key]['in'] = Setup::toYuan(array_sum($items[$key]['record_in_amount_cent']));
310
                }
311
            }
312
        }
313
        return $items;
314
    }
315
316
    /**
317
     * @param int $id
318
     * @param int $rating
319
     * @return int
320
     * @throws InvalidConfigException
321
     */
322
    public function updateRating(int $id, int $rating)
323
    {
324
        return Transaction::updateAll(
325
            ['rating' => $rating, 'updated_at' => Yii::$app->formatter->asDatetime('now')],
326
            ['id' => $id]
327
        );
328
    }
329
330
    /**
331
     * @param Account $account
332
     * @return bool
333
     * @throws \yii\db\Exception
334
     * @throws InvalidConfigException
335
     */
336
    public static function createAdjustRecord(Account $account)
337
    {
338
        $diff = $account->currency_balance_cent - AccountService::getCalculateCurrencyBalanceCent($account->id);
339
        if (!$diff) {
340
            return false;
341
        }
342
        $model = new Record();
343
        $model->direction = $diff > 0 ? DirectionType::INCOME : DirectionType::EXPENSE;
344
        $model->currency_amount_cent = abs($diff);
345
        $model->user_id = $account->user_id;
346
        $model->account_id = $account->id;
347
        $model->transaction_id = 0;
348
        $model->transaction_type = TransactionType::ADJUST;
349
        $model->category_id = CategoryService::getAdjustCategoryId();
0 ignored issues
show
Documentation Bug introduced by
It seems like app\core\services\Catego...::getAdjustCategoryId() can also be of type false or string. However, the property $category_id is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
350
        $model->currency_code = $account->currency_code;
351
        $model->date = self::getCreateRecordDate();
352
        if (!$model->save()) {
353
            Yii::error(
354
                ['request_id' => Yii::$app->requestId->id, $model->attributes, $model->errors],
355
                __FUNCTION__
356
            );
357
            throw new DBException(Setup::errorMessage($model->firstErrors));
358
        }
359
        return true;
360
    }
361
362
363
    /**
364
     * @param string $desc
365
     * @return array
366
     * @throws Exception
367
     */
368
    public static function matchTagsByDesc(string $desc): array
369
    {
370
        if ($tags = TagService::getTagNames()) {
371
            $tags = implode('|', $tags);
372
            preg_match_all("!({$tags})!", $desc, $matches);
373
            return data_get($matches, '0', []);
0 ignored issues
show
Bug Best Practice introduced by
The expression return data_get($matches, '0', array()) 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...
374
        }
375
        return [];
376
    }
377
378
    /**
379
     * @param array $tags
380
     * @throws InvalidConfigException
381
     */
382
    public static function createTags(array $tags)
383
    {
384
        $has = Tag::find()
385
            ->select('name')
386
            ->where(['user_id' => Yii::$app->user->id, 'name' => $tags])
387
            ->column();
388
        /** @var TagService $tagService */
389
        $tagService = Yii::createObject(TagService::class);
390
        foreach (array_diff($tags, $has) as $item) {
391
            try {
392
                $tagService->create(['name' => $item]);
393
            } catch (Exception $e) {
394
                Log::error('add tag fail', [$item, (string)$e]);
395
            }
396
        }
397
    }
398
399
    /**
400
     * @param string $desc
401
     * @return mixed|null
402
     * @throws Exception
403
     */
404
    public function getAmountByDesc(string $desc): float
405
    {
406
        // todo 支持简单的算数
407
        preg_match_all('!([0-9]+(?:\.[0-9]{1,2})?)!', $desc, $matches);
408
409
        if (count($matches[0])) {
410
            return array_pop($matches[0]);
411
        }
412
        return 0;
413
    }
414
415
    /**
416
     * @param array $rules
417
     * @param string $field
418
     * @param \Closure|array|null $callback
419
     * @return null|int|string
420
     * @throws Exception
421
     */
422
    private function getDataByDesc(array $rules, string $field, $callback = null)
423
    {
424
        foreach ($rules as $rule) {
425
            if ($data = data_get($rule->toArray(), $field)) {
426
                return $data;
427
            }
428
        }
429
        return $callback ? call_user_func($callback) : null;
430
    }
431
432
    /**
433
     * @return int
434
     * @throws Exception
435
     */
436
    public function getAccountIdByDesc(): int
437
    {
438
        $userId = Yii::$app->user->id;
439
        return (int)data_get(AccountService::getDefaultAccount($userId), 'id', 0);
0 ignored issues
show
Bug introduced by
It seems like $userId can also be of type string; however, parameter $userId of app\core\services\Accoun...ce::getDefaultAccount() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

439
        return (int)data_get(AccountService::getDefaultAccount(/** @scrutinizer ignore-type */ $userId), 'id', 0);
Loading history...
440
    }
441
442
    /**
443
     * @param string $desc
444
     * @return string date Y-m-d
445
     * @throws InvalidConfigException
446
     */
447
    private function getDateByDesc(string $desc): string
448
    {
449
        if (ArrayHelper::strPosArr($desc, ['昨天', '昨日']) !== false) {
450
            return self::getCreateRecordDate(time() - 3600 * 24 * 1);
451
        }
452
453
        if (ArrayHelper::strPosArr($desc, ['前天']) !== false) {
454
            return self::getCreateRecordDate(time() - 3600 * 24 * 2);
455
        }
456
457
        if (ArrayHelper::strPosArr($desc, ['大前天']) !== false) {
458
            return self::getCreateRecordDate(time() - 3600 * 24 * 3);
459
        }
460
461
        try {
462
            $time = self::getCreateRecordDate('now', 'php:H:i');
463
            preg_match_all('!([0-9]+)(月)([0-9]+)(号|日)!', $desc, $matches);
464
            if (($m = data_get($matches, '1.0')) && $d = data_get($matches, '3.0')) {
465
                $currMonth = Yii::$app->formatter->asDatetime('now', 'php:m');
466
                $y = Yii::$app->formatter->asDatetime($m > $currMonth ? strtotime('-1 year') : time(), 'php:Y');
467
                $m = sprintf("%02d", $m);
468
                $d = sprintf("%02d", $d);
469
                return "{$y}-{$m}-{$d} {$time}";
470
            }
471
472
            preg_match_all('!([0-9]+)(号|日)!', $desc, $matches);
473
            if ($d = data_get($matches, '1.0')) {
474
                $currDay = Yii::$app->formatter->asDatetime('now', 'php:d');
475
                $m = Yii::$app->formatter->asDatetime($d > $currDay ? strtotime('-1 month') : time(), 'php:Y-m');
476
                $d = sprintf("%02d", $d);
477
                return "{$m}-{$d} {$time}";
478
            }
479
        } catch (Exception $e) {
480
            Log::warning('未识别到日期', $desc);
481
        }
482
483
        return self::getCreateRecordDate();
484
    }
485
486
487
    /**
488
     * @param string $value
489
     * @param string $format
490
     * @return string
491
     * @throws InvalidConfigException
492
     */
493
    public static function getCreateRecordDate(string $value = 'now', string $format = 'php:Y-m-d H:i')
494
    {
495
        return Yii::$app->formatter->asDatetime($value, $format);
496
    }
497
498
499
    /**
500
     * @param string $tag
501
     * @param int $userId
502
     * @return bool|int|string|null
503
     */
504
    public static function countTransactionByTag(string $tag, int $userId)
505
    {
506
        return Transaction::find()
507
            ->where(['user_id' => $userId])
508
            ->andWhere(new Expression('FIND_IN_SET(:tag, tags)'))->addParams([':tag' => $tag])
509
            ->count();
510
    }
511
512
    /**
513
     * @param int $id
514
     * @param int $userId
515
     * @return Transaction|object
516
     * @throws NotFoundHttpException
517
     */
518
    public static function findCurrentOne(int $id, int $userId = 0): Transaction
519
    {
520
        $userId = $userId ?: Yii::$app->user->id;
521
        if (!$model = Transaction::find()->where(['id' => $id, 'user_id' => $userId])->one()) {
522
            throw new NotFoundHttpException('No data found');
523
        }
524
        return $model;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $model returns the type array which is incompatible with the type-hinted return app\core\models\Transaction.
Loading history...
525
    }
526
527
    /**
528
     * @return array
529
     * @throws Exception
530
     */
531
    public function exportData()
532
    {
533
        $data = [];
534
        $categoriesMap = CategoryService::getCurrentMap();
535
        $accountsMap = AccountService::getCurrentMap();
536
        $types = TransactionType::texts();
537
        $items = Transaction::find()
538
            ->where(['user_id' => Yii::$app->user->id])
539
            ->orderBy(['date' => SORT_DESC])
540
            ->asArray()
541
            ->all();
542
        foreach ($items as $k => $item) {
543
            $datum['date'] = $item['date'];
544
            $datum['category_name'] = data_get($categoriesMap, $item['category_id'], '');
545
            $datum['type'] = data_get($types, $item['type'], '');
546
            $datum['currency_amount'] = Setup::toYuan($item['currency_amount_cent']);
547
            $datum['tags'] = str_replace(',', '/', $item['tags']);
548
            $datum['description'] = $item['description'];
549
            $datum['remark'] = $item['remark'];
550
            $datum['account1'] = '';
551
            $datum['account2'] = '';
552
            switch ($item['type']) {
553
                case TransactionType::INCOME:
554
                    $datum['account1'] = data_get($accountsMap, $item['to_account_id'], '');
555
                    break;
556
                case TransactionType::EXPENSE:
557
                    $datum['account1'] = data_get($accountsMap, $item['from_account_id'], '');
558
                    break;
559
                case TransactionType::TRANSFER:
560
                    $datum['account1'] = data_get($accountsMap, $item['from_account_id'], '');
561
                    $datum['account2'] = data_get($accountsMap, $item['to_account_id'], '');
562
                    break;
563
                default:
564
                    # code...
565
                    break;
566
            }
567
            array_push($data, array_values($datum));
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $datum seems to be defined later in this foreach loop on line 543. Are you sure it is defined here?
Loading history...
568
        }
569
        return $data;
570
    }
571
}
572