141 lines
4.3 KiB
PHP
141 lines
4.3 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\AutomationRule;
|
|
use App\Models\Transaction;
|
|
use Illuminate\Support\Collection;
|
|
use JWadhams\JsonLogic;
|
|
|
|
class AutomationRuleService
|
|
{
|
|
public function applyRules(Transaction $transaction): void
|
|
{
|
|
if ($transaction->description_iv !== null) {
|
|
return;
|
|
}
|
|
|
|
$rules = AutomationRule::query()
|
|
->where('user_id', $transaction->user_id)
|
|
->with('labels')
|
|
->orderBy('priority')
|
|
->get();
|
|
|
|
if ($rules->isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
$transactionData = $this->prepareTransactionData($transaction);
|
|
$matchedRule = $this->evaluateRules($rules, $transactionData);
|
|
|
|
if ($matchedRule) {
|
|
$this->applyActions($transaction, $matchedRule);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return array{description: string, amount: float, transaction_date: string, bank_name: string, account_name: string, category: string|null, notes: string|null}
|
|
*/
|
|
private function prepareTransactionData(Transaction $transaction): array
|
|
{
|
|
$transaction->loadMissing(['account.bank', 'category']);
|
|
|
|
$account = $transaction->account;
|
|
$bank = $account?->bank;
|
|
|
|
$accountName = '';
|
|
if ($account && ! $account->encrypted) {
|
|
$accountName = trim($account->name);
|
|
}
|
|
|
|
return [
|
|
'description' => $this->normalizeWhitespace(mb_strtolower($transaction->description ?? '')),
|
|
'amount' => $transaction->amount / 100,
|
|
'transaction_date' => $transaction->transaction_date->format('Y-m-d'),
|
|
'bank_name' => mb_strtolower($bank->name ?? ''),
|
|
'account_name' => mb_strtolower($accountName),
|
|
'category' => $transaction->category?->name,
|
|
'notes' => $transaction->notes
|
|
? $this->normalizeWhitespace(mb_strtolower($transaction->notes))
|
|
: null,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param Collection<int, AutomationRule> $rules
|
|
* @param array<string, mixed> $transactionData
|
|
*/
|
|
private function evaluateRules(Collection $rules, array $transactionData): ?AutomationRule
|
|
{
|
|
foreach ($rules as $rule) {
|
|
try {
|
|
$normalizedRulesJson = $this->normalizeRuleJson($rule->rules_json);
|
|
$result = JsonLogic::apply($normalizedRulesJson, $transactionData);
|
|
|
|
if ($result === true) {
|
|
return $rule;
|
|
}
|
|
} catch (\Throwable) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function applyActions(Transaction $transaction, AutomationRule $rule): void
|
|
{
|
|
if ($rule->action_category_id) {
|
|
$transaction->category_id = $rule->action_category_id;
|
|
$transaction->saveQuietly();
|
|
}
|
|
|
|
$labelIds = $rule->labels->pluck('id')->all();
|
|
if (! empty($labelIds)) {
|
|
$transaction->labels()->syncWithoutDetaching($labelIds);
|
|
}
|
|
}
|
|
|
|
private function normalizeRuleJson(mixed $rulesJson): mixed
|
|
{
|
|
if (is_string($rulesJson)) {
|
|
$decoded = json_decode($rulesJson, true);
|
|
if (is_array($decoded)) {
|
|
return $this->normalizeRuleJson($decoded);
|
|
}
|
|
|
|
return mb_strtolower($rulesJson);
|
|
}
|
|
|
|
if (is_array($rulesJson)) {
|
|
if (array_is_list($rulesJson)) {
|
|
return array_map(function (mixed $item, int $index): mixed {
|
|
if ($index === 0 && is_string($item)) {
|
|
return mb_strtolower($item);
|
|
}
|
|
|
|
if (is_array($item) && isset($item['var']) && in_array($item['var'], ['description', 'notes'])) {
|
|
return $item;
|
|
}
|
|
|
|
return $this->normalizeRuleJson($item);
|
|
}, $rulesJson, array_keys($rulesJson));
|
|
}
|
|
|
|
$normalized = [];
|
|
foreach ($rulesJson as $key => $value) {
|
|
$normalized[$key] = $this->normalizeRuleJson($value);
|
|
}
|
|
|
|
return $normalized;
|
|
}
|
|
|
|
return $rulesJson;
|
|
}
|
|
|
|
private function normalizeWhitespace(string $str): string
|
|
{
|
|
return trim(preg_replace('/\s+/', ' ', $str));
|
|
}
|
|
}
|