Bicycle/App/Controller/AccountsController.php
Egor Isaev cff643ebc5 dev
2026-08-12 17:08:46 +03:00

220 lines
7.6 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* @package Bicycle
* @author Egor Isaev
* @description AccountsController.php
* @copyright (c) 12/08/2026
*/
namespace App\Controller;
use App\Classes\Currency;
use App\Repositories\AccountRateRepository;
use App\Repositories\AccountRepository;
use App\Repositories\UserRepository;
use Services\Auth;
use System\Classes\Controller;
use System\Classes\HTTP\HTTPException;
use System\Classes\MyException;
use System\Classes\Request;
use System\Classes\Validation;
/**
* CRUD счетов (accounts) — создание/правка/архивация. Ответы — JSON, вызывается
* ajax'ом с модалки на дашборде (см. App/view/Index/index.html, App/media/js/accounts.js) —
* своей страницы у счетов нет, см. бюджет_текущий_план.md → Goals → "UI — на дашборде".
*/
class AccountsController extends Controller
{
protected bool $_auth_protection = true;
/**
* @return string JSON
* @throws MyException
*/
public function createAction(): string
{
$request = Request::$current;
$user = $this->currentUser();
$validation = $this->buildValidation($request->post());
if (!$validation->check()) {
return $this->json(['success' => false, 'errors' => $validation->errors()], 422);
}
$summa = (float)$request->post('summa');
if ($summa < 0) {
return $this->json(['success' => false, 'errors' => ['summa' => 'Сумма не может быть отрицательной']], 422);
}
$type_acc = $request->post('type_acc');
$rate_percent = $request->post('rate_percent');
$data = [
'user_id' => $user->id,
'type_acc' => $type_acc,
'title' => $request->post('title'),
'summa' => $summa,
'currency' => strtoupper((string)$request->post('currency')),
'description' => $request->post('description') !== '' ? $request->post('description') : null,
'include_in_total' => $request->post('include_in_total') !== null,
'is_delete' => false,
'order' => 0,
];
$account_repo = new AccountRepository();
$account_repo->transaction(static function () use ($account_repo, $data, $type_acc, $rate_percent) {
$account_id = $account_repo->create($data);
if ($type_acc === 'savings' && $rate_percent !== null && $rate_percent !== '') {
(new AccountRateRepository())->create([
'account_id' => $account_id,
'rate_percent' => (float)$rate_percent,
'valid_from' => date('Y-m-d'),
'valid_to' => null,
]);
}
return $account_id;
});
return $this->json(['success' => true]);
}
/**
* @return string JSON
* @throws HTTPException|MyException
*/
public function editAction(): string
{
$request = Request::$current;
$account_repo = new AccountRepository();
$account = $this->ownAccount($account_repo, (int)$request->param(0));
$validation = $this->buildValidation($request->post());
if (!$validation->check()) {
return $this->json(['success' => false, 'errors' => $validation->errors()], 422);
}
$summa = (float)$request->post('summa');
if ($summa < 0) {
return $this->json(['success' => false, 'errors' => ['summa' => 'Сумма не может быть отрицательной']], 422);
}
$account_repo->update([
'id' => $account->id,
'type_acc' => $request->post('type_acc'),
'title' => $request->post('title'),
'summa' => $summa,
'currency' => strtoupper((string)$request->post('currency')),
'description' => $request->post('description') !== '' ? $request->post('description') : null,
'include_in_total' => $request->post('include_in_total') !== null,
]);
return $this->json(['success' => true]);
}
/**
* Архивация (is_delete = 1), не физическое удаление — см. бюджет_текущий_план.md → Accounts.
*
* @return string JSON
* @throws HTTPException|MyException
*/
public function deleteAction(): string
{
$request = Request::$current;
$account_repo = new AccountRepository();
$account = $this->ownAccount($account_repo, (int)$request->param(0));
$account_repo->update(['id' => $account->id, 'is_delete' => true]);
return $this->json(['success' => true]);
}
/**
* Возврат из архива (is_delete = 0).
*
* @return string JSON
* @throws HTTPException|MyException
*/
public function restoreAction(): string
{
$request = Request::$current;
$account_repo = new AccountRepository();
$account = $this->ownAccount($account_repo, (int)$request->param(0));
$account_repo->update(['id' => $account->id, 'is_delete' => false]);
return $this->json(['success' => true]);
}
/**
* Общие правила валидации формы счёта (create/edit).
*
* @param array $data
* @return Validation
*/
protected function buildValidation(array $data): Validation
{
return Validation::factory($data)
->label('title', 'Название')
->label('type_acc', 'Тип')
->label('summa', 'Сумма')
->label('currency', 'Валюта')
->rule('title', 'required')
->rule('title', 'max_length', [128])
->rule('type_acc', 'required')
->rule('type_acc', 'in', [['cash', 'bank', 'savings']])
->rule('summa', 'required')
->rule('summa', 'numeric')
->rule('currency', 'required')
->rule('currency', 'in', [array_keys(Currency::LIST)])
->rule('description', 'max_length', [255]);
}
/**
* Бюджетный пользователь (App\Repositories\UserRepository), связанный с текущим логином
* Services\Auth — см. App\Controller\IndexController за тем же паттерном.
*
* @return object
* @throws HTTPException|MyException
*/
protected function currentUser(): object
{
$auth_user = Auth::instance()->getUser();
$user = (new UserRepository())->getByLogin($auth_user['login']);
if (!$user) {
throw HTTPException::factory(404);
}
return $user;
}
/**
* Счёт по id, принадлежащий текущему пользователю — иначе 404 (не 403, чтобы не подтверждать
* чужому пользователю сам факт существования id).
*
* @param AccountRepository $account_repo
* @param int $id
* @return object
* @throws HTTPException|MyException
*/
protected function ownAccount(AccountRepository $account_repo, int $id): object
{
$user = $this->currentUser();
$account = $account_repo->get($id);
if (!$account || (int)$account->user_id !== (int)$user->id) {
throw HTTPException::factory(404);
}
return $account;
}
}