🤖 Auto commit for release 'develop' on 2026-02-06

This commit is contained in:
JC5
2026-02-06 13:55:17 +01:00
parent b4d01d464d
commit 2de9926db8
324 changed files with 14224 additions and 14136 deletions
@@ -123,16 +123,6 @@ class IndexController extends Controller
]);
}
private function subtract(array $startBalances, array $endBalances): array
{
$result = [];
foreach ($endBalances as $key => $value) {
$result[$key] = bcsub((string) $value, $startBalances[$key] ?? '0');
}
return $result;
}
/**
* Show list of accounts.
*
@@ -214,4 +204,14 @@ class IndexController extends Controller
'accounts' => $accounts,
]);
}
private function subtract(array $startBalances, array $endBalances): array
{
$result = [];
foreach ($endBalances as $key => $value) {
$result[$key] = bcsub((string) $value, $startBalances[$key] ?? '0');
}
return $result;
}
}
@@ -104,29 +104,6 @@ class ForgotPasswordController extends Controller
return back()->with('status', trans($response));
}
/**
* @throws FireflyException
*/
private function validateHost(): void
{
try {
$configuredHost = parse_url((string) config('app.url'), PHP_URL_HOST);
} catch (UrlException $e) {
throw new FireflyException('Please set a valid and correct Firefly III URL in the APP_URL environment variable.', 0, $e);
}
if (!is_string($configuredHost)) {
throw new FireflyException('Please set a valid and correct Firefly III URL in the APP_URL environment variable.');
}
$host = request()->host();
if ($configuredHost !== $host) {
Log::error(sprintf('Host header is "%s", APP_URL is "%s".', $host, $configuredHost));
throw new FireflyException(
'The Host-header does not match the host in the APP_URL environment variable. Please make sure these match. See also: https://bit.ly/FF3-host-header'
);
}
}
/**
* Show form for email recovery.
*
@@ -155,4 +132,27 @@ class ForgotPasswordController extends Controller
return view('auth.passwords.email')->with(['allowRegistration' => $allowRegistration, 'pageTitle' => $pageTitle]);
}
/**
* @throws FireflyException
*/
private function validateHost(): void
{
try {
$configuredHost = parse_url((string) config('app.url'), PHP_URL_HOST);
} catch (UrlException $e) {
throw new FireflyException('Please set a valid and correct Firefly III URL in the APP_URL environment variable.', 0, $e);
}
if (!is_string($configuredHost)) {
throw new FireflyException('Please set a valid and correct Firefly III URL in the APP_URL environment variable.');
}
$host = request()->host();
if ($configuredHost !== $host) {
Log::error(sprintf('Host header is "%s", APP_URL is "%s".', $host, $configuredHost));
throw new FireflyException(
'The Host-header does not match the host in the APP_URL environment variable. Please make sure these match. See also: https://bit.ly/FF3-host-header'
);
}
}
}
+23 -23
View File
@@ -150,29 +150,6 @@ class LoginController extends Controller
return response()->json();
}
/**
* Get the login username to be used by the controller.
*/
public function username(): string
{
return $this->username;
}
/**
* Get the failed login response instance.
*
* @SuppressWarnings("PHPMD.UnusedFormalParameter")
*
* @throws ValidationException
*/
protected function sendFailedLoginResponse(Request $request): void
{
$exception = ValidationException::withMessages([$this->username() => [trans('auth.failed')]]);
$exception->redirectTo = route('login');
throw $exception;
}
/**
* Log the user out of the application.
*/
@@ -257,6 +234,29 @@ class LoginController extends Controller
]);
}
/**
* Get the login username to be used by the controller.
*/
public function username(): string
{
return $this->username;
}
/**
* Get the failed login response instance.
*
* @SuppressWarnings("PHPMD.UnusedFormalParameter")
*
* @throws ValidationException
*/
protected function sendFailedLoginResponse(Request $request): void
{
$exception = ValidationException::withMessages([$this->username() => [trans('auth.failed')]]);
$exception->redirectTo = route('login');
throw $exception;
}
/**
* Send the response after the user was authenticated.
*
@@ -113,31 +113,6 @@ class RegisterController extends Controller
return redirect($this->redirectPath());
}
/**
* @throws FireflyException
*/
protected function allowedToRegister(): bool
{
// is allowed to register?
$allowRegistration = true;
try {
$singleUserMode = FireflyConfig::get('single_user_mode', config('firefly.configuration.single_user_mode'))->data;
} catch (ContainerExceptionInterface|NotFoundExceptionInterface) {
$singleUserMode = true;
}
$userCount = User::count();
$guard = config('auth.defaults.guard');
if (true === $singleUserMode && $userCount > 0 && 'web' === $guard) {
$allowRegistration = false;
}
if ('web' !== $guard) {
return false;
}
return $allowRegistration;
}
/**
* Show the application registration form if the invitation code is valid.
*
@@ -197,4 +172,29 @@ class RegisterController extends Controller
return view('auth.register', ['isDemoSite' => $isDemoSite, 'email' => $email, 'pageTitle' => $pageTitle]);
}
/**
* @throws FireflyException
*/
protected function allowedToRegister(): bool
{
// is allowed to register?
$allowRegistration = true;
try {
$singleUserMode = FireflyConfig::get('single_user_mode', config('firefly.configuration.single_user_mode'))->data;
} catch (ContainerExceptionInterface|NotFoundExceptionInterface) {
$singleUserMode = true;
}
$userCount = User::count();
$guard = config('auth.defaults.guard');
if (true === $singleUserMode && $userCount > 0 && 'web' === $guard) {
$allowRegistration = false;
}
if ('web' !== $guard) {
return false;
}
return $allowRegistration;
}
}
@@ -126,22 +126,23 @@ class TwoFactorController extends Controller
return redirect(route('home'));
}
/**
* Each MFA history has a timestamp and a code, saving the MFA entries for 5 minutes. So if the
* submitted MFA code has been submitted in the last 5 minutes, it won't work despite being valid.
*/
private function inMFAHistory(string $mfaCode, array $mfaHistory): bool
private function addToMFAFailureCounter(): void
{
$now = Carbon::now()->getTimestamp();
foreach ($mfaHistory as $entry) {
$time = $entry['time'];
$code = $entry['code'];
if ($code === $mfaCode && ($now - $time) <= 300) {
return true;
}
}
$preference = (int) Preferences::get('mfa_failure_count', 0)->data;
++$preference;
Log::channel('audit')->info(sprintf('MFA failure count is set to %d.', $preference));
Preferences::set('mfa_failure_count', $preference);
}
return false;
private function addToMFAHistory(string $mfaCode): void
{
/** @var array $mfaHistory */
$mfaHistory = Preferences::get('mfa_history', [])->data;
$entry = ['time' => Carbon::now()->getTimestamp(), 'code' => $mfaCode];
$mfaHistory[] = $entry;
Preferences::set('mfa_history', $mfaHistory);
$this->filterMFAHistory();
}
/**
@@ -163,14 +164,6 @@ class TwoFactorController extends Controller
Preferences::set('mfa_history', $newHistory);
}
private function addToMFAFailureCounter(): void
{
$preference = (int) Preferences::get('mfa_failure_count', 0)->data;
++$preference;
Log::channel('audit')->info(sprintf('MFA failure count is set to %d.', $preference));
Preferences::set('mfa_failure_count', $preference);
}
private function getMFAFailureCounter(): int
{
$value = (int) Preferences::get('mfa_failure_count', 0)->data;
@@ -179,21 +172,22 @@ class TwoFactorController extends Controller
return $value;
}
private function addToMFAHistory(string $mfaCode): void
/**
* Each MFA history has a timestamp and a code, saving the MFA entries for 5 minutes. So if the
* submitted MFA code has been submitted in the last 5 minutes, it won't work despite being valid.
*/
private function inMFAHistory(string $mfaCode, array $mfaHistory): bool
{
/** @var array $mfaHistory */
$mfaHistory = Preferences::get('mfa_history', [])->data;
$entry = ['time' => Carbon::now()->getTimestamp(), 'code' => $mfaCode];
$mfaHistory[] = $entry;
$now = Carbon::now()->getTimestamp();
foreach ($mfaHistory as $entry) {
$time = $entry['time'];
$code = $entry['code'];
if ($code === $mfaCode && ($now - $time) <= 300) {
return true;
}
}
Preferences::set('mfa_history', $mfaHistory);
$this->filterMFAHistory();
}
private function resetMFAFailureCounter(): void
{
Preferences::set('mfa_failure_count', 0);
Log::channel('audit')->info('MFA failure count is set to zero.');
return false;
}
/**
@@ -235,4 +229,10 @@ class TwoFactorController extends Controller
Preferences::set('mfa_recovery', $newList);
}
private function resetMFAFailureCounter(): void
{
Preferences::set('mfa_failure_count', 0);
Log::channel('audit')->info('MFA failure count is set to zero.');
}
}
+52 -52
View File
@@ -136,6 +136,58 @@ class IndexController extends Controller
return view('bills.index', ['bills' => $bills, 'sums' => $sums, 'total' => $total, 'totals' => $totals, 'today' => $today]);
}
/**
* Set the order of a bill.
*/
public function setOrder(Request $request, Bill $bill): JsonResponse
{
$objectGroupTitle = (string) $request->get('objectGroupTitle');
$newOrder = (int) $request->get('order');
$this->repository->setOrder($bill, $newOrder);
if ('' !== $objectGroupTitle) {
$this->repository->setObjectGroup($bill, $objectGroupTitle);
}
if ('' === $objectGroupTitle) {
$this->repository->removeObjectGroup($bill);
}
return response()->json(['data' => 'OK']);
}
private function amountPerPeriod(array $bill, string $range): string
{
$avg = bcdiv(bcadd((string) $bill['amount_min'], (string) $bill['amount_max']), '2');
Log::debug(sprintf('Amount per period for bill #%d "%s"', $bill['id'], $bill['name']));
Log::debug(sprintf('Average is %s', $avg));
// calculate amount per year:
$multiplies = ['yearly' => '1', 'half-year' => '2', 'quarterly' => '4', 'monthly' => '12', 'weekly' => '52.17', 'daily' => '365.24'];
$yearAmount = bcmul($avg, bcdiv($multiplies[$bill['repeat_freq']], (string) ($bill['skip'] + 1)));
Log::debug(sprintf('Amount per year is %s (%s * %s / %s)', $yearAmount, $avg, $multiplies[$bill['repeat_freq']], (string) ($bill['skip'] + 1)));
// per period:
$division = [
'1Y' => '1',
'6M' => '2',
'3M' => '4',
'1M' => '12',
'1W' => '52.16',
'1D' => '365.24',
'YTD' => '1',
'QTD' => '4',
'MTD' => '12',
'last7' => '52.16',
'last30' => '12',
'last90' => '4',
'last365' => '1',
];
$perPeriod = bcdiv($yearAmount, $division[$range]);
Log::debug(sprintf('Amount per %s is %s (%s / %s)', $range, $perPeriod, $yearAmount, $division[$range]));
return $perPeriod;
}
private function getSums(array $bills): array
{
Log::debug(sprintf('now in getSums(count:%d)', count($bills)));
@@ -220,40 +272,6 @@ class IndexController extends Controller
return $sums;
}
private function amountPerPeriod(array $bill, string $range): string
{
$avg = bcdiv(bcadd((string) $bill['amount_min'], (string) $bill['amount_max']), '2');
Log::debug(sprintf('Amount per period for bill #%d "%s"', $bill['id'], $bill['name']));
Log::debug(sprintf('Average is %s', $avg));
// calculate amount per year:
$multiplies = ['yearly' => '1', 'half-year' => '2', 'quarterly' => '4', 'monthly' => '12', 'weekly' => '52.17', 'daily' => '365.24'];
$yearAmount = bcmul($avg, bcdiv($multiplies[$bill['repeat_freq']], (string) ($bill['skip'] + 1)));
Log::debug(sprintf('Amount per year is %s (%s * %s / %s)', $yearAmount, $avg, $multiplies[$bill['repeat_freq']], (string) ($bill['skip'] + 1)));
// per period:
$division = [
'1Y' => '1',
'6M' => '2',
'3M' => '4',
'1M' => '12',
'1W' => '52.16',
'1D' => '365.24',
'YTD' => '1',
'QTD' => '4',
'MTD' => '12',
'last7' => '52.16',
'last30' => '12',
'last90' => '4',
'last365' => '1',
];
$perPeriod = bcdiv($yearAmount, $division[$range]);
Log::debug(sprintf('Amount per %s is %s (%s / %s)', $range, $perPeriod, $yearAmount, $division[$range]));
return $perPeriod;
}
private function getTotals(array $sums): array
{
$totals = [];
@@ -287,22 +305,4 @@ class IndexController extends Controller
return $totals;
}
/**
* Set the order of a bill.
*/
public function setOrder(Request $request, Bill $bill): JsonResponse
{
$objectGroupTitle = (string) $request->get('objectGroupTitle');
$newOrder = (int) $request->get('order');
$this->repository->setOrder($bill, $newOrder);
if ('' !== $objectGroupTitle) {
$this->repository->setObjectGroup($bill, $objectGroupTitle);
}
if ('' === $objectGroupTitle) {
$this->repository->removeObjectGroup($bill);
}
return response()->json(['data' => 'OK']);
}
}
@@ -97,7 +97,7 @@ class BudgetLimitController extends Controller
return true;
});
return view('budgets.budget-limits.create', ['start' => $start, 'end' => $end, 'currencies' => $currencies, 'budget' => $budget]);
return view('budgets.budget-limits.create', ['start' => $start, 'end' => $end, 'currencies' => $currencies, 'budget' => $budget]);
}
public function delete(BudgetLimit $budgetLimit): Redirector|RedirectResponse
+18 -18
View File
@@ -158,6 +158,24 @@ final class IndexController extends Controller
]);
}
public function reorder(Request $request, BudgetRepositoryInterface $repository): JsonResponse
{
$this->abRepository->cleanup();
$budgetIds = $request->get('budgetIds');
foreach ($budgetIds as $index => $budgetId) {
$budgetId = (int) $budgetId;
$budget = $repository->find($budgetId);
if ($budget instanceof Budget) {
Log::debug(sprintf('Set budget #%d ("%s") to position %d', $budget->id, $budget->name, $index + 1));
$repository->setBudgetOrder($budget, $index + 1);
}
}
Preferences::mark();
return response()->json(['OK']);
}
private function getAllAvailableBudgets(Carbon $start, Carbon $end): array
{
Log::debug(sprintf('Start of getAllAvailableBudgets("%s", "%s")', $start->format('Y-m-d H:i:s'), $end->format('Y-m-d H:i:s')));
@@ -305,22 +323,4 @@ final class IndexController extends Controller
return $sums;
}
public function reorder(Request $request, BudgetRepositoryInterface $repository): JsonResponse
{
$this->abRepository->cleanup();
$budgetIds = $request->get('budgetIds');
foreach ($budgetIds as $index => $budgetId) {
$budgetId = (int) $budgetId;
$budget = $repository->find($budgetId);
if ($budget instanceof Budget) {
Log::debug(sprintf('Set budget #%d ("%s") to position %d', $budget->id, $budget->name, $index + 1));
$repository->setBudgetOrder($budget, $index + 1);
}
}
Preferences::mark();
return response()->json(['OK']);
}
}
@@ -204,17 +204,6 @@ class AccountController extends Controller
return response()->json($data);
}
/**
* Expenses per budget for all time, as shown on account overview.
*/
public function expenseBudgetAll(AccountRepositoryInterface $repository, Account $account): JsonResponse
{
$start = $repository->oldestJournalDate($account) ?? today(config('app.timezone'))->startOfMonth();
$end = today(config('app.timezone'));
return $this->expenseBudget($account, $start, $end);
}
/**
* Expenses per budget, as shown on account overview.
*/
@@ -293,14 +282,14 @@ class AccountController extends Controller
}
/**
* Expenses grouped by category for account.
* Expenses per budget for all time, as shown on account overview.
*/
public function expenseCategoryAll(AccountRepositoryInterface $repository, Account $account): JsonResponse
public function expenseBudgetAll(AccountRepositoryInterface $repository, Account $account): JsonResponse
{
$start = $repository->oldestJournalDate($account) ?? today(config('app.timezone'))->startOfMonth();
$end = today(config('app.timezone'));
return $this->expenseCategory($account, $start, $end);
return $this->expenseBudget($account, $start, $end);
}
/**
@@ -375,6 +364,17 @@ class AccountController extends Controller
return response()->json($data);
}
/**
* Expenses grouped by category for account.
*/
public function expenseCategoryAll(AccountRepositoryInterface $repository, Account $account): JsonResponse
{
$start = $repository->oldestJournalDate($account) ?? today(config('app.timezone'))->startOfMonth();
$end = today(config('app.timezone'));
return $this->expenseCategory($account, $start, $end);
}
/**
* Shows the balances for all the user's frontpage accounts.
*
@@ -401,17 +401,6 @@ class AccountController extends Controller
return response()->json($this->accountBalanceChart($accounts, $start, $end));
}
/**
* Shows the income grouped by category for an account, in all time.
*/
public function incomeCategoryAll(AccountRepositoryInterface $repository, Account $account): JsonResponse
{
$start = $repository->oldestJournalDate($account) ?? today(config('app.timezone'))->startOfMonth();
$end = today(config('app.timezone'));
return $this->incomeCategory($account, $start, $end);
}
/**
* Shows all income per account for each category.
*/
@@ -485,6 +474,17 @@ class AccountController extends Controller
return response()->json($data);
}
/**
* Shows the income grouped by category for an account, in all time.
*/
public function incomeCategoryAll(AccountRepositoryInterface $repository, Account $account): JsonResponse
{
$start = $repository->oldestJournalDate($account) ?? today(config('app.timezone'))->startOfMonth();
$end = today(config('app.timezone'));
return $this->incomeCategory($account, $start, $end);
}
/**
* Shows overview of account during a single period.
*
@@ -617,15 +617,6 @@ class AccountController extends Controller
return response()->json($data);
}
private function updateChartKeys(array $array, array $balances): array
{
foreach (array_keys($balances) as $key) {
$array[$key] ??= ['key' => $key];
}
return $array;
}
/**
* Shows the balances for a given set of dates and accounts.
*
@@ -762,4 +753,13 @@ class AccountController extends Controller
return response()->json($data);
}
private function updateChartKeys(array $array, array $balances): array
{
foreach (array_keys($balances) as $key) {
$array[$key] ??= ['key' => $key];
}
return $array;
}
}
@@ -196,23 +196,6 @@ class BudgetReportController extends Controller
return response()->json($data);
}
private function makeEntries(Carbon $start, Carbon $end): array
{
$return = [];
$format = Navigation::preferredCarbonLocalizedFormat($start, $end);
$preferredRange = Navigation::preferredRangeFormat($start, $end);
$currentStart = clone $start;
while ($currentStart <= $end) {
$currentEnd = Navigation::endOfPeriod($currentStart, $preferredRange);
$key = $currentStart->isoFormat($format);
$return[$key] = '0';
$currentStart = clone $currentEnd;
$currentStart->addDay()->startOfDay();
}
return $return;
}
/**
* Chart that groups expenses by the account.
*/
@@ -242,4 +225,21 @@ class BudgetReportController extends Controller
return response()->json($data);
}
private function makeEntries(Carbon $start, Carbon $end): array
{
$return = [];
$format = Navigation::preferredCarbonLocalizedFormat($start, $end);
$preferredRange = Navigation::preferredRangeFormat($start, $end);
$currentStart = clone $start;
while ($currentStart <= $end) {
$currentEnd = Navigation::endOfPeriod($currentStart, $preferredRange);
$key = $currentStart->isoFormat($format);
$return[$key] = '0';
$currentStart = clone $currentEnd;
$currentStart->addDay()->startOfDay();
}
return $return;
}
}
@@ -97,11 +97,6 @@ class CategoryController extends Controller
return response()->json($data);
}
private function getDate(): Carbon
{
return today(config('app.timezone'));
}
/**
* Shows the category chart on the front page.
* TODO test method for category refactor.
@@ -151,6 +146,67 @@ class CategoryController extends Controller
return response()->json($data);
}
/**
* Chart for period for transactions without a category.
* TODO test me.
*/
public function reportPeriodNoCategory(Collection $accounts, Carbon $start, Carbon $end): JsonResponse
{
$cache = new CacheProperties();
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty('chart.category.period.no-cat');
$cache->addProperty($accounts->pluck('id')->toArray());
if ($cache->has()) {
return response()->json($cache->get());
}
$data = $this->reportPeriodChart($accounts, $start, $end, null);
$cache->store($data);
return response()->json($data);
}
/**
* Chart for a specific period.
* TODO test me, for category refactor.
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function specificPeriod(Category $category, Carbon $date): JsonResponse
{
$range = Navigation::getViewRange(false);
$start = Navigation::startOfPeriod($date, $range);
$end = session()->get('end');
if ($end < $start) {
[$end, $start] = [$start, $end];
}
$cache = new CacheProperties();
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty($category->id);
$cache->addProperty('chart.category.period-chart');
if ($cache->has()) {
return response()->json($cache->get());
}
/** @var WholePeriodChartGenerator $chartGenerator */
$chartGenerator = app(WholePeriodChartGenerator::class);
$chartData = $chartGenerator->generate($category, $start, $end);
$data = $this->generator->multiSet($chartData);
$cache->store($data);
return response()->json($data);
}
private function getDate(): Carbon
{
return today(config('app.timezone'));
}
/**
* Generate report chart for either with or without category.
*/
@@ -227,60 +283,4 @@ class CategoryController extends Controller
return $this->generator->multiSet($chartData);
}
/**
* Chart for period for transactions without a category.
* TODO test me.
*/
public function reportPeriodNoCategory(Collection $accounts, Carbon $start, Carbon $end): JsonResponse
{
$cache = new CacheProperties();
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty('chart.category.period.no-cat');
$cache->addProperty($accounts->pluck('id')->toArray());
if ($cache->has()) {
return response()->json($cache->get());
}
$data = $this->reportPeriodChart($accounts, $start, $end, null);
$cache->store($data);
return response()->json($data);
}
/**
* Chart for a specific period.
* TODO test me, for category refactor.
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function specificPeriod(Category $category, Carbon $date): JsonResponse
{
$range = Navigation::getViewRange(false);
$start = Navigation::startOfPeriod($date, $range);
$end = session()->get('end');
if ($end < $start) {
[$end, $start] = [$start, $end];
}
$cache = new CacheProperties();
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty($category->id);
$cache->addProperty('chart.category.period-chart');
if ($cache->has()) {
return response()->json($cache->get());
}
/** @var WholePeriodChartGenerator $chartGenerator */
$chartGenerator = app(WholePeriodChartGenerator::class);
$chartData = $chartGenerator->generate($category, $start, $end);
$data = $this->generator->multiSet($chartData);
$cache->store($data);
return response()->json($data);
}
}
@@ -268,26 +268,6 @@ class CategoryReportController extends Controller
return response()->json($data);
}
/**
* TODO duplicate function
*/
private function makeEntries(Carbon $start, Carbon $end): array
{
$return = [];
$format = Navigation::preferredCarbonLocalizedFormat($start, $end);
$preferredRange = Navigation::preferredRangeFormat($start, $end);
$currentStart = clone $start;
while ($currentStart <= $end) {
$currentEnd = Navigation::endOfPeriod($currentStart, $preferredRange);
$key = $currentStart->isoFormat($format);
$return[$key] = '0';
$currentStart = clone $currentEnd;
$currentStart->addDay()->startOfDay();
}
return $return;
}
public function sourceExpense(Collection $accounts, Collection $categories, Carbon $start, Carbon $end): JsonResponse
{
$result = [];
@@ -343,4 +323,24 @@ class CategoryReportController extends Controller
return response()->json($data);
}
/**
* TODO duplicate function
*/
private function makeEntries(Carbon $start, Carbon $end): array
{
$return = [];
$format = Navigation::preferredCarbonLocalizedFormat($start, $end);
$preferredRange = Navigation::preferredRangeFormat($start, $end);
$currentStart = clone $start;
while ($currentStart <= $end) {
$currentEnd = Navigation::endOfPeriod($currentStart, $preferredRange);
$key = $currentStart->isoFormat($format);
$return[$key] = '0';
$currentStart = clone $currentEnd;
$currentStart->addDay()->startOfDay();
}
return $return;
}
}
@@ -213,44 +213,6 @@ class DoubleReportController extends Controller
return response()->json($data);
}
/**
* TODO duplicate function
*/
private function getCounterpartName(Collection $accounts, int $id, string $name, ?string $iban): string
{
/** @var Account $account */
foreach ($accounts as $account) {
if ($account->name === $name && $account->id !== $id) {
return $account->name;
}
if (null !== $account->iban && $account->iban === $iban && $account->id !== $id) {
return $account->iban;
}
}
return $name;
}
/**
* TODO duplicate function
*/
private function makeEntries(Carbon $start, Carbon $end): array
{
$return = [];
$format = Navigation::preferredCarbonLocalizedFormat($start, $end);
$preferredRange = Navigation::preferredRangeFormat($start, $end);
$currentStart = clone $start;
while ($currentStart <= $end) {
$currentEnd = Navigation::endOfPeriod($currentStart, $preferredRange);
$key = $currentStart->isoFormat($format);
$return[$key] = '0';
$currentStart = clone $currentEnd;
$currentStart->addDay()->startOfDay();
}
return $return;
}
public function tagExpense(Collection $accounts, Collection $others, Carbon $start, Carbon $end): JsonResponse
{
$result = [];
@@ -356,4 +318,42 @@ class DoubleReportController extends Controller
return response()->json($data);
}
/**
* TODO duplicate function
*/
private function getCounterpartName(Collection $accounts, int $id, string $name, ?string $iban): string
{
/** @var Account $account */
foreach ($accounts as $account) {
if ($account->name === $name && $account->id !== $id) {
return $account->name;
}
if (null !== $account->iban && $account->iban === $iban && $account->id !== $id) {
return $account->iban;
}
}
return $name;
}
/**
* TODO duplicate function
*/
private function makeEntries(Carbon $start, Carbon $end): array
{
$return = [];
$format = Navigation::preferredCarbonLocalizedFormat($start, $end);
$preferredRange = Navigation::preferredRangeFormat($start, $end);
$currentStart = clone $start;
while ($currentStart <= $end) {
$currentEnd = Navigation::endOfPeriod($currentStart, $preferredRange);
$key = $currentStart->isoFormat($format);
$return[$key] = '0';
$currentStart = clone $currentEnd;
$currentStart->addDay()->startOfDay();
}
return $return;
}
}
@@ -274,26 +274,6 @@ class TagReportController extends Controller
return response()->json($data);
}
/**
* TODO duplicate function
*/
private function makeEntries(Carbon $start, Carbon $end): array
{
$return = [];
$format = Navigation::preferredCarbonLocalizedFormat($start, $end);
$preferredRange = Navigation::preferredRangeFormat($start, $end);
$currentStart = clone $start;
while ($currentStart <= $end) {
$currentEnd = Navigation::endOfPeriod($currentStart, $preferredRange);
$key = $currentStart->isoFormat($format);
$return[$key] = '0';
$currentStart = clone $currentEnd;
$currentStart->addDay()->startOfDay();
}
return $return;
}
public function sourceExpense(Collection $accounts, Collection $tags, Carbon $start, Carbon $end): JsonResponse
{
$result = [];
@@ -401,4 +381,24 @@ class TagReportController extends Controller
return response()->json($data);
}
/**
* TODO duplicate function
*/
private function makeEntries(Carbon $start, Carbon $end): array
{
$return = [];
$format = Navigation::preferredCarbonLocalizedFormat($start, $end);
$preferredRange = Navigation::preferredRangeFormat($start, $end);
$currentStart = clone $start;
while ($currentStart <= $end) {
$currentEnd = Navigation::endOfPeriod($currentStart, $preferredRange);
$key = $currentStart->isoFormat($format);
$return[$key] = '0';
$currentStart = clone $currentEnd;
$currentStart->addDay()->startOfDay();
}
return $return;
}
}
+203 -203
View File
@@ -75,6 +75,11 @@ class DebugController extends Controller
$this->middleware(IsDemoUser::class)->except(['displayError']);
}
public function apiTest(): View
{
return view('test.api-test');
}
/**
* Show all possible errors.
*
@@ -162,200 +167,6 @@ class DebugController extends Controller
return view('debug', ['table' => $table, 'now' => $now, 'logContent' => $logContent]);
}
public function apiTest(): View
{
return view('test.api-test');
}
private function generateTable(): string
{
// system information:
$system = $this->getSystemInformation();
$docker = $this->getBuildInfo();
$app = $this->getAppInfo();
$user = $this->getUserInfo();
return (string) view('partials.debug-table', ['system' => $system, 'docker' => $docker, 'app' => $app, 'user' => $user]);
}
private function getSystemInformation(): array
{
$maxFileSize = Steam::phpBytes(ini_get('upload_max_filesize'));
$maxPostSize = Steam::phpBytes(ini_get('post_max_size'));
$drivers = DB::availableDrivers();
$currentDriver = DB::getDriverName();
return [
'php_version' => PHP_VERSION,
'php_os' => PHP_OS,
'build_time' => config('firefly.build_time'),
'build_time_nice' => Carbon::parse(config('firefly.build_time'), 'Europe/Amsterdam')->setTimezone('Europe/Amsterdam')->format('Y-m-d H:i:s e'),
'uname' => php_uname('m'),
'interface' => PHP_SAPI,
'bits' => PHP_INT_SIZE * 8,
'bcscale' => bcscale(),
'display_errors' => ini_get('display_errors'),
'error_reporting' => $this->errorReporting((int) ini_get('error_reporting')),
'upload_size' => min($maxFileSize, $maxPostSize),
'all_drivers' => $drivers,
'current_driver' => $currentDriver,
];
}
private function getBuildInfo(): array
{
$return = [
'is_docker' => env('IS_DOCKER', false), // @phpstan-ignore-line
'build' => '(unknown)',
'build_date' => '(unknown)',
'base_build' => '(unknown)',
'base_build_date' => '(unknown)',
];
try {
if (file_exists('/var/www/counter-main.txt')) {
$return['build'] = trim(file_get_contents('/var/www/counter-main.txt'));
Log::debug(sprintf('build is now "%s"', $return['build']));
}
} catch (Exception $e) {
Log::debug('Could not check build counter, but thats ok.');
Log::warning($e->getMessage());
}
try {
if (file_exists('/var/www/build-date-main.txt')) {
$return['build_date'] = trim(file_get_contents('/var/www/build-date-main.txt'));
}
} catch (Exception $e) {
Log::debug('Could not check build date, but thats ok.');
Log::warning($e->getMessage());
}
if ('' !== (string) env('BASE_IMAGE_BUILD')) { // @phpstan-ignore-line
$return['base_build'] = env('BASE_IMAGE_BUILD'); // @phpstan-ignore-line
}
if ('' !== (string) env('BASE_IMAGE_DATE')) { // @phpstan-ignore-line
$return['base_build_date'] = env('BASE_IMAGE_DATE'); // @phpstan-ignore-line
}
return $return;
}
private function getAppInfo(): array
{
$userGuard = config('auth.defaults.guard');
$config = FireflyConfig::get('last_rt_job', 0);
$lastTime = (int) $config->data;
$lastCronjob = 'never';
$lastCronjobAgo = 'never';
if ($lastTime > 0) {
$carbon = Carbon::createFromTimestamp($lastTime);
$lastCronjob = $carbon->format('Y-m-d H:i:s');
$lastCronjobAgo = $carbon->locale('en')->diffForHumans(); // @phpstan-ignore-line
}
return [
'debug' => var_export(config('app.debug'), true),
'audit_log_channel' => envNonEmpty('AUDIT_LOG_CHANNEL', '(empty)'),
'default_language' => (string) config('firefly.default_language'),
'default_locale' => (string) config('firefly.default_locale'),
'remote_header' => 'remote_user_guard' === $userGuard ? config('auth.guard_header') : 'N/A',
'remote_mail_header' => 'remote_user_guard' === $userGuard ? config('auth.guard_email') : 'N/A',
'stateful_domains' => implode(', ', config('sanctum.stateful')),
// the dates for the cron job are based on the recurring cron job's times.
// any of the cron jobs will do, they always run at the same time.
// but this job is the oldest, so the biggest chance it ran once
'last_cronjob' => $lastCronjob,
'last_cronjob_ago' => $lastCronjobAgo,
];
}
private function getUserInfo(): array
{
$userFlags = $this->getUserFlags();
// user info
$userAgent = request()->header('user-agent');
// set languages, see what happens:
$original = setlocale(LC_ALL, '0');
$localeAttempts = [];
$parts = Steam::getLocaleArray(Steam::getLocale());
foreach ($parts as $code) {
$code = trim($code);
Log::debug(sprintf('Trying to set %s', $code));
$result = setlocale(LC_ALL, $code);
$localeAttempts[$code] = $result === $code;
}
setlocale(LC_ALL, (string) $original);
return [
'user_id' => auth()->user()->id,
'user_count' => User::count(),
'user_flags' => $userFlags,
'user_agent' => $userAgent,
'primary' => Amount::getPrimaryCurrency(),
'convert_to_primary' => Amount::convertToPrimary(),
'locale_attempts' => $localeAttempts,
'locale' => Steam::getLocale(),
'language' => Steam::getLanguage(),
'view_range' => Preferences::get('viewRange', '1M')->data,
];
}
private function getUserFlags(): string
{
$flags = [];
/** @var User $user */
$user = auth()->user();
// has liabilities
if ($user->accounts()->accountTypeIn([AccountTypeEnum::DEBT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::MORTGAGE->value])->count() > 0) {
$flags[] = '<span title="Has liabilities">:credit_card:</span>';
}
// has piggies
$repository = app(PiggyBankRepositoryInterface::class);
$repository->setUser($user);
if ($repository->getPiggyBanks()->count() > 0) {
$flags[] = '<span title="Has piggy banks">:pig:</span>';
}
// has stored reconciliations
$type = TransactionType::whereType(TransactionTypeEnum::RECONCILIATION->value)->first();
if ($user->transactionJournals()->where('transaction_type_id', $type->id)->count() > 0) {
$flags[] = '<span title="Has reconciled">:ledger:</span>';
}
// has used importer?
// has rules
if ($user->rules()->count() > 0) {
$flags[] = '<span title="Has rules">:wrench:</span>';
}
// has recurring transactions
if ($user->recurrences()->count() > 0) {
$flags[] = '<span title="Has recurring transactions">:clock130:</span>';
}
// has groups
if ($user->objectGroups()->count() > 0) {
$flags[] = '<span title="Has object groups">:bookmark_tabs:</span>';
}
// uses bills
if ($user->bills()->count() > 0) {
$flags[] = '<span title="Has subscriptions">:email:</span>';
}
return implode(' ', $flags);
}
public function routes(Request $request): never
{
if (!auth()->user()->hasRole('owner')) {
@@ -456,6 +267,100 @@ class DebugController extends Controller
exit;
}
/**
* Flash all types of messages.
*/
public function testFlash(Request $request): Redirector|RedirectResponse
{
$request->session()->flash('success', 'This is a success message.');
$request->session()->flash('info', 'This is an info message.');
$request->session()->flash('warning', 'This is a warning.');
$request->session()->flash('error', 'This is an error!');
return redirect(route('home'));
}
private function generateTable(): string
{
// system information:
$system = $this->getSystemInformation();
$docker = $this->getBuildInfo();
$app = $this->getAppInfo();
$user = $this->getUserInfo();
return (string) view('partials.debug-table', ['system' => $system, 'docker' => $docker, 'app' => $app, 'user' => $user]);
}
private function getAppInfo(): array
{
$userGuard = config('auth.defaults.guard');
$config = FireflyConfig::get('last_rt_job', 0);
$lastTime = (int) $config->data;
$lastCronjob = 'never';
$lastCronjobAgo = 'never';
if ($lastTime > 0) {
$carbon = Carbon::createFromTimestamp($lastTime);
$lastCronjob = $carbon->format('Y-m-d H:i:s');
$lastCronjobAgo = $carbon->locale('en')->diffForHumans(); // @phpstan-ignore-line
}
return [
'debug' => var_export(config('app.debug'), true),
'audit_log_channel' => envNonEmpty('AUDIT_LOG_CHANNEL', '(empty)'),
'default_language' => (string) config('firefly.default_language'),
'default_locale' => (string) config('firefly.default_locale'),
'remote_header' => 'remote_user_guard' === $userGuard ? config('auth.guard_header') : 'N/A',
'remote_mail_header' => 'remote_user_guard' === $userGuard ? config('auth.guard_email') : 'N/A',
'stateful_domains' => implode(', ', config('sanctum.stateful')),
// the dates for the cron job are based on the recurring cron job's times.
// any of the cron jobs will do, they always run at the same time.
// but this job is the oldest, so the biggest chance it ran once
'last_cronjob' => $lastCronjob,
'last_cronjob_ago' => $lastCronjobAgo,
];
}
private function getBuildInfo(): array
{
$return = [
'is_docker' => env('IS_DOCKER', false), // @phpstan-ignore-line
'build' => '(unknown)',
'build_date' => '(unknown)',
'base_build' => '(unknown)',
'base_build_date' => '(unknown)',
];
try {
if (file_exists('/var/www/counter-main.txt')) {
$return['build'] = trim(file_get_contents('/var/www/counter-main.txt'));
Log::debug(sprintf('build is now "%s"', $return['build']));
}
} catch (Exception $e) {
Log::debug('Could not check build counter, but thats ok.');
Log::warning($e->getMessage());
}
try {
if (file_exists('/var/www/build-date-main.txt')) {
$return['build_date'] = trim(file_get_contents('/var/www/build-date-main.txt'));
}
} catch (Exception $e) {
Log::debug('Could not check build date, but thats ok.');
Log::warning($e->getMessage());
}
if ('' !== (string) env('BASE_IMAGE_BUILD')) { // @phpstan-ignore-line
$return['base_build'] = env('BASE_IMAGE_BUILD'); // @phpstan-ignore-line
}
if ('' !== (string) env('BASE_IMAGE_DATE')) { // @phpstan-ignore-line
$return['base_build_date'] = env('BASE_IMAGE_DATE'); // @phpstan-ignore-line
}
return $return;
}
private function getParameter(string $name): string
{
switch ($name) {
@@ -552,16 +457,111 @@ class DebugController extends Controller
}
}
/**
* Flash all types of messages.
*/
public function testFlash(Request $request): Redirector|RedirectResponse
private function getSystemInformation(): array
{
$request->session()->flash('success', 'This is a success message.');
$request->session()->flash('info', 'This is an info message.');
$request->session()->flash('warning', 'This is a warning.');
$request->session()->flash('error', 'This is an error!');
$maxFileSize = Steam::phpBytes(ini_get('upload_max_filesize'));
$maxPostSize = Steam::phpBytes(ini_get('post_max_size'));
$drivers = DB::availableDrivers();
$currentDriver = DB::getDriverName();
return redirect(route('home'));
return [
'php_version' => PHP_VERSION,
'php_os' => PHP_OS,
'build_time' => config('firefly.build_time'),
'build_time_nice' => Carbon::parse(config('firefly.build_time'), 'Europe/Amsterdam')->setTimezone('Europe/Amsterdam')->format('Y-m-d H:i:s e'),
'uname' => php_uname('m'),
'interface' => PHP_SAPI,
'bits' => PHP_INT_SIZE * 8,
'bcscale' => bcscale(),
'display_errors' => ini_get('display_errors'),
'error_reporting' => $this->errorReporting((int) ini_get('error_reporting')),
'upload_size' => min($maxFileSize, $maxPostSize),
'all_drivers' => $drivers,
'current_driver' => $currentDriver,
];
}
private function getUserFlags(): string
{
$flags = [];
/** @var User $user */
$user = auth()->user();
// has liabilities
if ($user->accounts()->accountTypeIn([AccountTypeEnum::DEBT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::MORTGAGE->value])->count() > 0) {
$flags[] = '<span title="Has liabilities">:credit_card:</span>';
}
// has piggies
$repository = app(PiggyBankRepositoryInterface::class);
$repository->setUser($user);
if ($repository->getPiggyBanks()->count() > 0) {
$flags[] = '<span title="Has piggy banks">:pig:</span>';
}
// has stored reconciliations
$type = TransactionType::whereType(TransactionTypeEnum::RECONCILIATION->value)->first();
if ($user->transactionJournals()->where('transaction_type_id', $type->id)->count() > 0) {
$flags[] = '<span title="Has reconciled">:ledger:</span>';
}
// has used importer?
// has rules
if ($user->rules()->count() > 0) {
$flags[] = '<span title="Has rules">:wrench:</span>';
}
// has recurring transactions
if ($user->recurrences()->count() > 0) {
$flags[] = '<span title="Has recurring transactions">:clock130:</span>';
}
// has groups
if ($user->objectGroups()->count() > 0) {
$flags[] = '<span title="Has object groups">:bookmark_tabs:</span>';
}
// uses bills
if ($user->bills()->count() > 0) {
$flags[] = '<span title="Has subscriptions">:email:</span>';
}
return implode(' ', $flags);
}
private function getUserInfo(): array
{
$userFlags = $this->getUserFlags();
// user info
$userAgent = request()->header('user-agent');
// set languages, see what happens:
$original = setlocale(LC_ALL, '0');
$localeAttempts = [];
$parts = Steam::getLocaleArray(Steam::getLocale());
foreach ($parts as $code) {
$code = trim($code);
Log::debug(sprintf('Trying to set %s', $code));
$result = setlocale(LC_ALL, $code);
$localeAttempts[$code] = $result === $code;
}
setlocale(LC_ALL, (string) $original);
return [
'user_id' => auth()->user()->id,
'user_count' => User::count(),
'user_flags' => $userFlags,
'user_agent' => $userAgent,
'primary' => Amount::getPrimaryCurrency(),
'convert_to_primary' => Amount::convertToPrimary(),
'locale_attempts' => $localeAttempts,
'locale' => Steam::getLocale(),
'language' => Steam::getLanguage(),
'view_range' => Preferences::get('viewRange', '1M')->data,
];
}
}
@@ -162,37 +162,6 @@ class ReconcileController extends Controller
return response()->json($return);
}
private function processJournal(Account $account, TransactionCurrency $currency, array $journal, string $amount): string
{
$toAdd = '0';
Log::debug(sprintf('User submitted %s #%d: "%s"', $journal['transaction_type_type'], $journal['transaction_journal_id'], $journal['description']));
// not much magic below we need to cover using tests.
if ($account->id === $journal['source_account_id']) {
if ($currency->id === $journal['currency_id']) {
$toAdd = $journal['amount'];
}
if (null !== $journal['foreign_currency_id'] && $journal['foreign_currency_id'] === $currency->id) {
$toAdd = $journal['foreign_amount'];
}
}
if ($account->id === $journal['destination_account_id']) {
if ($currency->id === $journal['currency_id']) {
$toAdd = bcmul((string) $journal['amount'], '-1');
}
if (null !== $journal['foreign_currency_id'] && $journal['foreign_currency_id'] === $currency->id) {
$toAdd = bcmul((string) $journal['foreign_amount'], '-1');
}
}
Log::debug(sprintf('Going to add %s to %s', $toAdd, $amount));
$amount = bcadd($amount, (string) $toAdd);
Log::debug(sprintf('Result is %s', $amount));
return $amount;
}
/**
* Returns a list of transactions in a modal.
*
@@ -278,6 +247,37 @@ class ReconcileController extends Controller
return response()->json(['html' => $html, 'startBalance' => $startBalance, 'endBalance' => $endBalance]);
}
private function processJournal(Account $account, TransactionCurrency $currency, array $journal, string $amount): string
{
$toAdd = '0';
Log::debug(sprintf('User submitted %s #%d: "%s"', $journal['transaction_type_type'], $journal['transaction_journal_id'], $journal['description']));
// not much magic below we need to cover using tests.
if ($account->id === $journal['source_account_id']) {
if ($currency->id === $journal['currency_id']) {
$toAdd = $journal['amount'];
}
if (null !== $journal['foreign_currency_id'] && $journal['foreign_currency_id'] === $currency->id) {
$toAdd = $journal['foreign_amount'];
}
}
if ($account->id === $journal['destination_account_id']) {
if ($currency->id === $journal['currency_id']) {
$toAdd = bcmul((string) $journal['amount'], '-1');
}
if (null !== $journal['foreign_currency_id'] && $journal['foreign_currency_id'] === $currency->id) {
$toAdd = bcmul((string) $journal['foreign_amount'], '-1');
}
}
Log::debug(sprintf('Going to add %s to %s', $toAdd, $amount));
$amount = bcadd($amount, (string) $toAdd);
Log::debug(sprintf('Result is %s', $amount));
return $amount;
}
/**
* "fix" amounts to make it easier on the reconciliation overview:
*/
@@ -63,14 +63,6 @@ class EditController extends Controller
});
}
public function resetHistory(PiggyBank $piggyBank): RedirectResponse
{
$this->piggyRepos->resetHistory($piggyBank);
session()->flash('success', (string) trans('firefly.piggy_history_reset'));
return redirect(route('piggy-banks.show', [$piggyBank->id]));
}
/**
* Edit a piggy bank.
*
@@ -117,6 +109,14 @@ class EditController extends Controller
]);
}
public function resetHistory(PiggyBank $piggyBank): RedirectResponse
{
$this->piggyRepos->resetHistory($piggyBank);
session()->flash('success', (string) trans('firefly.piggy_history_reset'));
return redirect(route('piggy-banks.show', [$piggyBank->id]));
}
/**
* Update a piggy bank.
*/
@@ -102,36 +102,22 @@ class IndexController extends Controller
return view('piggy-banks.index', ['piggyBanks' => $piggyBanks, 'accounts' => $accounts]);
}
private function groupPiggyBanks(Collection $collection): array
/**
* Set the order of a piggy bank.
*/
public function setOrder(Request $request, PiggyBank $piggyBank): JsonResponse
{
/** @var PiggyBankTransformer $transformer */
$transformer = app(PiggyBankTransformer::class);
$transformer->setParameters(new ParameterBag());
$piggyBanks = [];
// enrich
/** @var User $admin */
$admin = auth()->user();
$enrichment = new PiggyBankEnrichment();
$enrichment->setUser($admin);
$collection = $enrichment->enrich($collection);
/** @var PiggyBank $piggy */
foreach ($collection as $piggy) {
$array = $transformer->transform($piggy);
$groupOrder = (int) $array['object_group_order'];
$piggyBanks[$groupOrder] ??= [
'object_group_id' => $array['object_group_id'] ?? 0,
'object_group_title' => $array['object_group_title'] ?? trans('firefly.default_group_title_name'),
'piggy_banks' => [],
];
$array['attachments'] = $this->piggyRepos->getAttachments($piggy);
// sum the total amount for the index.
$piggyBanks[$groupOrder]['piggy_banks'][] = $array;
$objectGroupTitle = (string) $request->get('objectGroupTitle');
$newOrder = (int) $request->get('order');
$this->piggyRepos->setOrder($piggyBank, $newOrder);
if ('' !== $objectGroupTitle) {
$this->piggyRepos->setObjectGroup($piggyBank, $objectGroupTitle);
}
if ('' === $objectGroupTitle) {
$this->piggyRepos->removeObjectGroup($piggyBank);
}
return $piggyBanks;
return response()->json(['data' => 'OK']);
}
private function collectAccounts(Collection $collection): array
@@ -178,30 +164,36 @@ class IndexController extends Controller
return $return;
}
private function mergeAccountsAndPiggies(array $piggyBanks, array $accounts): array
private function groupPiggyBanks(Collection $collection): array
{
/** @var array $group */
foreach ($piggyBanks as $group) {
/** @var array $piggyBank */
foreach ($group['piggy_banks'] as $piggyBank) {
// loop all accounts in this piggy bank subtract the current amount from "left to save" in the $accounts array.
/** @var array $piggyAccount */
foreach ($piggyBank['accounts'] as $piggyAccount) {
$accountId = $piggyAccount['account_id'];
if (array_key_exists($accountId, $accounts)) {
$accounts[$accountId]['left'] = bcsub((string) $accounts[$accountId]['left'], (string) $piggyAccount['current_amount']);
$accounts[$accountId]['saved'] = bcadd((string) $accounts[$accountId]['saved'], (string) $piggyAccount['current_amount']);
$accounts[$accountId]['target'] = bcadd((string) $accounts[$accountId]['target'], (string) $piggyBank['target_amount']);
$accounts[$accountId]['to_save'] = bcadd((string) $accounts[$accountId]['to_save'], bcsub(
(string) $piggyBank['target_amount'],
(string) $piggyAccount['current_amount']
));
}
}
}
/** @var PiggyBankTransformer $transformer */
$transformer = app(PiggyBankTransformer::class);
$transformer->setParameters(new ParameterBag());
$piggyBanks = [];
// enrich
/** @var User $admin */
$admin = auth()->user();
$enrichment = new PiggyBankEnrichment();
$enrichment->setUser($admin);
$collection = $enrichment->enrich($collection);
/** @var PiggyBank $piggy */
foreach ($collection as $piggy) {
$array = $transformer->transform($piggy);
$groupOrder = (int) $array['object_group_order'];
$piggyBanks[$groupOrder] ??= [
'object_group_id' => $array['object_group_id'] ?? 0,
'object_group_title' => $array['object_group_title'] ?? trans('firefly.default_group_title_name'),
'piggy_banks' => [],
];
$array['attachments'] = $this->piggyRepos->getAttachments($piggy);
// sum the total amount for the index.
$piggyBanks[$groupOrder]['piggy_banks'][] = $array;
}
return $accounts;
return $piggyBanks;
}
private function makeSums(array $piggyBanks): array
@@ -239,21 +231,29 @@ class IndexController extends Controller
return $piggyBanks;
}
/**
* Set the order of a piggy bank.
*/
public function setOrder(Request $request, PiggyBank $piggyBank): JsonResponse
private function mergeAccountsAndPiggies(array $piggyBanks, array $accounts): array
{
$objectGroupTitle = (string) $request->get('objectGroupTitle');
$newOrder = (int) $request->get('order');
$this->piggyRepos->setOrder($piggyBank, $newOrder);
if ('' !== $objectGroupTitle) {
$this->piggyRepos->setObjectGroup($piggyBank, $objectGroupTitle);
}
if ('' === $objectGroupTitle) {
$this->piggyRepos->removeObjectGroup($piggyBank);
/** @var array $group */
foreach ($piggyBanks as $group) {
/** @var array $piggyBank */
foreach ($group['piggy_banks'] as $piggyBank) {
// loop all accounts in this piggy bank subtract the current amount from "left to save" in the $accounts array.
/** @var array $piggyAccount */
foreach ($piggyBank['accounts'] as $piggyAccount) {
$accountId = $piggyAccount['account_id'];
if (array_key_exists($accountId, $accounts)) {
$accounts[$accountId]['left'] = bcsub((string) $accounts[$accountId]['left'], (string) $piggyAccount['current_amount']);
$accounts[$accountId]['saved'] = bcadd((string) $accounts[$accountId]['saved'], (string) $piggyAccount['current_amount']);
$accounts[$accountId]['target'] = bcadd((string) $accounts[$accountId]['target'], (string) $piggyBank['target_amount']);
$accounts[$accountId]['to_save'] = bcadd((string) $accounts[$accountId]['to_save'], bcsub(
(string) $piggyBank['target_amount'],
(string) $piggyAccount['current_amount']
));
}
}
}
}
return response()->json(['data' => 'OK']);
return $accounts;
}
}
+15 -15
View File
@@ -274,6 +274,21 @@ class MfaController extends Controller
return redirect(route('profile.mfa.backup-codes'));
}
public function index(): Factory|RedirectResponse|View
{
if (!$this->internalAuth) {
request()->session()->flash('error', trans('firefly.external_user_mgt_disabled'));
return redirect(route('profile.index'));
}
$subTitle = (string) trans('firefly.mfa_index_title');
$subTitleIcon = 'fa-calculator';
$enabledMFA = null !== auth()->user()->mfa_secret;
return view('profile.mfa.index')->with(['subTitle' => $subTitle, 'subTitleIcon' => $subTitleIcon, 'enabledMFA' => $enabledMFA]);
}
/**
* TODO duplicate code.
*
@@ -309,19 +324,4 @@ class MfaController extends Controller
}
Preferences::set('mfa_history', $newHistory);
}
public function index(): Factory|RedirectResponse|View
{
if (!$this->internalAuth) {
request()->session()->flash('error', trans('firefly.external_user_mgt_disabled'));
return redirect(route('profile.index'));
}
$subTitle = (string) trans('firefly.mfa_index_title');
$subTitleIcon = 'fa-calculator';
$enabledMFA = null !== auth()->user()->mfa_secret;
return view('profile.mfa.index')->with(['subTitle' => $subTitle, 'subTitleIcon' => $subTitleIcon, 'enabledMFA' => $enabledMFA]);
}
}
+39 -39
View File
@@ -85,6 +85,45 @@ class ProfileController extends Controller
$this->middleware(IsDemoUser::class)->except(['index']);
}
/**
* Change your email address.
*/
public function changeEmail(Request $request): Factory|RedirectResponse|View
{
if (!$this->internalAuth) {
$request->session()->flash('error', trans('firefly.external_user_mgt_disabled'));
return redirect(route('profile.index'));
}
$title = auth()->user()->email;
$email = auth()->user()->email;
$subTitle = (string) trans('firefly.change_your_email');
$subTitleIcon = 'fa-envelope';
return view('profile.change-email', ['title' => $title, 'subTitle' => $subTitle, 'subTitleIcon' => $subTitleIcon, 'email' => $email]);
}
/**
* Change your password.
*
* @return Factory|Redirector|RedirectResponse|View
*/
public function changePassword(Request $request): Factory|\Illuminate\Contracts\View\View|Redirector|RedirectResponse
{
if (!$this->internalAuth) {
$request->session()->flash('error', trans('firefly.external_user_mgt_disabled'));
return redirect(route('profile.index'));
}
$title = auth()->user()->email;
$subTitle = (string) trans('firefly.change_your_password');
$subTitleIcon = 'fa-key';
return view('profile.change-password', ['title' => $title, 'subTitle' => $subTitle, 'subTitleIcon' => $subTitleIcon]);
}
/**
* Screen to confirm email change.
*
@@ -242,25 +281,6 @@ class ProfileController extends Controller
return redirect(route('index'));
}
/**
* Change your email address.
*/
public function changeEmail(Request $request): Factory|RedirectResponse|View
{
if (!$this->internalAuth) {
$request->session()->flash('error', trans('firefly.external_user_mgt_disabled'));
return redirect(route('profile.index'));
}
$title = auth()->user()->email;
$email = auth()->user()->email;
$subTitle = (string) trans('firefly.change_your_email');
$subTitleIcon = 'fa-envelope';
return view('profile.change-email', ['title' => $title, 'subTitle' => $subTitle, 'subTitleIcon' => $subTitleIcon, 'email' => $email]);
}
/**
* Submit change password form.
*/
@@ -293,26 +313,6 @@ class ProfileController extends Controller
return redirect(route('profile.index'));
}
/**
* Change your password.
*
* @return Factory|Redirector|RedirectResponse|View
*/
public function changePassword(Request $request): Factory|\Illuminate\Contracts\View\View|Redirector|RedirectResponse
{
if (!$this->internalAuth) {
$request->session()->flash('error', trans('firefly.external_user_mgt_disabled'));
return redirect(route('profile.index'));
}
$title = auth()->user()->email;
$subTitle = (string) trans('firefly.change_your_password');
$subTitleIcon = 'fa-key';
return view('profile.change-password', ['title' => $title, 'subTitle' => $subTitle, 'subTitleIcon' => $subTitleIcon]);
}
/**
* Submit delete account.
*/
@@ -278,24 +278,6 @@ class DoubleController extends Controller
return view('reports.double.partials.accounts', ['sums' => $sums, 'report' => $report]);
}
/**
* TODO this method is duplicated.
*/
private function getCounterpartName(Collection $accounts, int $id, string $name, ?string $iban): string
{
/** @var Account $account */
foreach ($accounts as $account) {
if ($account->name === $name && $account->id !== $id) {
return $account->name;
}
if (null !== $account->iban && $account->iban === $iban && $account->id !== $id) {
return $account->iban;
}
}
return $name;
}
/**
* @return Factory|View
*/
@@ -486,4 +468,22 @@ class DoubleController extends Controller
return $result;
}
/**
* TODO this method is duplicated.
*/
private function getCounterpartName(Collection $accounts, int $id, string $name, ?string $iban): string
{
/** @var Account $account */
foreach ($accounts as $account) {
if ($account->name === $name && $account->id !== $id) {
return $account->name;
}
if (null !== $account->iban && $account->iban === $iban && $account->id !== $id) {
return $account->iban;
}
}
return $name;
}
}
+28 -28
View File
@@ -151,6 +151,34 @@ class EditController extends Controller
]);
}
/**
* Update the rule.
*
* @return Redirector|RedirectResponse
*/
public function update(RuleFormRequest $request, Rule $rule)
{
$data = $request->getRuleData();
$this->ruleRepos->update($rule, $data);
session()->flash('success', (string) trans('firefly.updated_rule', ['title' => $rule->title]));
Preferences::mark();
$redirect = redirect($this->getPreviousUrl('rules.edit.url'));
if (true === $data['run_after_form']) {
return redirect(route('rules.select-transactions', [$rule->id]));
}
if (1 === (int) $request->get('return_to_edit')) {
session()->put('rules.edit.fromUpdate', true);
$redirect = redirect(route('rules.edit', [$rule->id]))->withInput(['return_to_edit' => 1]);
}
return $redirect;
}
/**
* @throws FireflyException
*/
@@ -189,32 +217,4 @@ class EditController extends Controller
return $renderedEntries;
}
/**
* Update the rule.
*
* @return Redirector|RedirectResponse
*/
public function update(RuleFormRequest $request, Rule $rule)
{
$data = $request->getRuleData();
$this->ruleRepos->update($rule, $data);
session()->flash('success', (string) trans('firefly.updated_rule', ['title' => $rule->title]));
Preferences::mark();
$redirect = redirect($this->getPreviousUrl('rules.edit.url'));
if (true === $data['run_after_form']) {
return redirect(route('rules.select-transactions', [$rule->id]));
}
if (1 === (int) $request->get('return_to_edit')) {
session()->put('rules.edit.fromUpdate', true);
$redirect = redirect(route('rules.edit', [$rule->id]))->withInput(['return_to_edit' => 1]);
}
return $redirect;
}
}
@@ -89,6 +89,23 @@ class InstallController extends Controller
return view('install.index');
}
/**
* Create specific RSA keys.
*/
public function keys(): void
{
$key = RSA::createKey(4096);
[$publicKey, $privateKey] = [Passport::keyPath('oauth-public.key'), Passport::keyPath('oauth-private.key')];
if (file_exists($publicKey) || file_exists($privateKey)) {
return;
}
file_put_contents($publicKey, (string) $key->getPublicKey());
file_put_contents($privateKey, $key->toString('PKCS1'));
}
public function runCommand(Request $request): JsonResponse
{
$requestIndex = (int) $request->get('index');
@@ -149,21 +166,4 @@ class InstallController extends Controller
return true;
}
/**
* Create specific RSA keys.
*/
public function keys(): void
{
$key = RSA::createKey(4096);
[$publicKey, $privateKey] = [Passport::keyPath('oauth-public.key'), Passport::keyPath('oauth-private.key')];
if (file_exists($publicKey) || file_exists($privateKey)) {
return;
}
file_put_contents($publicKey, (string) $key->getPublicKey());
file_put_contents($privateKey, $key->toString('PKCS1'));
}
}
+14 -14
View File
@@ -113,6 +113,20 @@ class TagController extends Controller
return view('tags.delete', ['tag' => $tag, 'subTitle' => $subTitle]);
}
/**
* Destroy a tag.
*/
public function destroy(Tag $tag): RedirectResponse
{
$tagName = $tag->tag;
$this->repository->destroy($tag);
session()->flash('success', (string) trans('firefly.deleted_tag', ['tag' => $tagName]));
Preferences::mark();
return redirect($this->getPreviousUrl('tags.delete.url'));
}
/**
* Edit a tag.
*
@@ -198,20 +212,6 @@ class TagController extends Controller
return redirect(route('tags.index'));
}
/**
* Destroy a tag.
*/
public function destroy(Tag $tag): RedirectResponse
{
$tagName = $tag->tag;
$this->repository->destroy($tag);
session()->flash('success', (string) trans('firefly.deleted_tag', ['tag' => $tagName]));
Preferences::mark();
return redirect($this->getPreviousUrl('tags.delete.url'));
}
/**
* Show a single tag.
*
@@ -129,7 +129,6 @@ class BulkController extends Controller
event(new UpdatedSingleTransactionGroup($flags, $objects));
event(new WebhookMessagesRequestSending());
Preferences::mark();
$request->session()->flash('success', trans_choice('firefly.mass_edited_transactions_success', $count));
@@ -148,6 +147,17 @@ class BulkController extends Controller
return true;
}
private function updateJournalCategory(TransactionJournal $journal, bool $ignoreUpdate, string $category): bool
{
if ($ignoreUpdate) {
return false;
}
Log::debug(sprintf('Set budget to %s', $category));
$this->repository->updateCategory($journal, $category);
return true;
}
private function updateJournalTags(TransactionJournal $journal, string $action, array $tags): bool
{
if ('do_replace' === $action) {
@@ -162,15 +172,4 @@ class BulkController extends Controller
return true;
}
private function updateJournalCategory(TransactionJournal $journal, bool $ignoreUpdate, string $category): bool
{
if ($ignoreUpdate) {
return false;
}
Log::debug(sprintf('Set budget to %s', $category));
$this->repository->updateCategory($journal, $category);
return true;
}
}
@@ -138,149 +138,6 @@ class ConvertController extends Controller
]);
}
private function getValidDepositSources(): array
{
// make repositories
$liabilityTypes = [AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::CREDITCARD->value, AccountTypeEnum::LOAN->value];
$accountList = $this->accountRepository->getActiveAccountsByType([
AccountTypeEnum::REVENUE->value,
AccountTypeEnum::CASH->value,
AccountTypeEnum::LOAN->value,
AccountTypeEnum::DEBT->value,
AccountTypeEnum::MORTGAGE->value,
]);
$grouped = [];
// group accounts:
/** @var Account $account */
foreach ($accountList as $account) {
$role = (string) $this->accountRepository->getMetaValue($account, 'account_role');
$name = $account->name;
if ('' === $role) {
$role = 'no_account_type';
}
// maybe it's a liability thing:
if (in_array($account->accountType->type, $liabilityTypes, true)) {
$role = 'l_'.$account->accountType->type;
}
if (AccountTypeEnum::CASH->value === $account->accountType->type) {
$role = 'cash_account';
$name = sprintf('(%s)', trans('firefly.cash'));
}
if (AccountTypeEnum::REVENUE->value === $account->accountType->type) {
$role = 'revenue_account';
}
$key = (string) trans('firefly.opt_group_'.$role);
$grouped[$key][$account->id] = $name;
}
return $grouped;
}
private function getValidWithdrawalDests(): array
{
// make repositories
$liabilityTypes = [AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::CREDITCARD->value, AccountTypeEnum::LOAN->value];
$accountList = $this->accountRepository->getActiveAccountsByType([
AccountTypeEnum::EXPENSE->value,
AccountTypeEnum::CASH->value,
AccountTypeEnum::LOAN->value,
AccountTypeEnum::DEBT->value,
AccountTypeEnum::MORTGAGE->value,
]);
$grouped = [];
// group accounts:
/** @var Account $account */
foreach ($accountList as $account) {
$role = (string) $this->accountRepository->getMetaValue($account, 'account_role');
$name = $account->name;
if ('' === $role) {
$role = 'no_account_type';
}
// maybe it's a liability thing:
if (in_array($account->accountType->type, $liabilityTypes, true)) {
$role = 'l_'.$account->accountType->type;
}
if (AccountTypeEnum::CASH->value === $account->accountType->type) {
$role = 'cash_account';
$name = sprintf('(%s)', trans('firefly.cash'));
}
if (AccountTypeEnum::EXPENSE->value === $account->accountType->type) {
$role = 'expense_account';
}
$key = (string) trans('firefly.opt_group_'.$role);
$grouped[$key][$account->id] = $name;
}
return $grouped;
}
/**
* @throws Exception
*/
private function getLiabilities(): array
{
// make repositories
$accountList = $this->accountRepository->getActiveAccountsByType([
AccountTypeEnum::LOAN->value,
AccountTypeEnum::DEBT->value,
AccountTypeEnum::MORTGAGE->value,
]);
$grouped = [];
// group accounts:
/** @var Account $account */
foreach ($accountList as $account) {
$date = today()->endOfDay();
Log::debug(sprintf('getLiabilities: Call finalAccountBalance with date/time "%s"', $date->toIso8601String()));
// 2025-10-08 replace finalAccountBalance with accountsBalancesOptimized.
// $balance = Steam::finalAccountBalance($account, $date)['balance'];
$balance = Steam::accountsBalancesOptimized(new Collection()->push($account), $date)[$account->id]['balance'] ?? '0';
$currency = $this->accountRepository->getAccountCurrency($account) ?? $this->primaryCurrency;
$role = sprintf('l_%s', $account->accountType->type);
$key = (string) trans(sprintf('firefly.opt_group_%s', $role));
$grouped[$key][$account->id] = sprintf('%s (%s)', $account->name, Amount::formatAnything($currency, $balance, false));
}
return $grouped;
}
/**
* @throws Exception
*/
private function getAssetAccounts(): array
{
// make repositories
$accountList = $this->accountRepository->getActiveAccountsByType([AccountTypeEnum::ASSET->value]);
$grouped = [];
// group accounts:
/** @var Account $account */
foreach ($accountList as $account) {
$date = today()->endOfDay();
Log::debug(sprintf('getAssetAccounts: Call finalAccountBalance with date/time "%s"', $date->toIso8601String()));
// 2025-10-08 replace finalAccountBalance with accountsBalancesOptimized.
// $balance = Steam::finalAccountBalance($account, $date)['balance'];
$balance = Steam::accountsBalancesOptimized(new Collection()->push($account), $date)[$account->id]['balance'] ?? '0';
$currency = $this->accountRepository->getAccountCurrency($account) ?? $this->primaryCurrency;
$role = (string) $this->accountRepository->getMetaValue($account, 'account_role');
if ('' === $role) {
$role = 'no_account_type';
}
$key = (string) trans(sprintf('firefly.opt_group_%s', $role));
$grouped[$key][$account->id] = sprintf('%s (%s)', $account->name, Amount::formatAnything($currency, $balance, false));
}
return $grouped;
}
/**
* Do the conversion.
*
@@ -313,7 +170,6 @@ class ConvertController extends Controller
event(new UpdatedSingleTransactionGroup($flags, $objects));
event(new WebhookMessagesRequestSending());
return redirect(route('transactions.show', [$group->id]));
}
@@ -404,4 +260,147 @@ class ConvertController extends Controller
return $journal;
}
/**
* @throws Exception
*/
private function getAssetAccounts(): array
{
// make repositories
$accountList = $this->accountRepository->getActiveAccountsByType([AccountTypeEnum::ASSET->value]);
$grouped = [];
// group accounts:
/** @var Account $account */
foreach ($accountList as $account) {
$date = today()->endOfDay();
Log::debug(sprintf('getAssetAccounts: Call finalAccountBalance with date/time "%s"', $date->toIso8601String()));
// 2025-10-08 replace finalAccountBalance with accountsBalancesOptimized.
// $balance = Steam::finalAccountBalance($account, $date)['balance'];
$balance = Steam::accountsBalancesOptimized(new Collection()->push($account), $date)[$account->id]['balance'] ?? '0';
$currency = $this->accountRepository->getAccountCurrency($account) ?? $this->primaryCurrency;
$role = (string) $this->accountRepository->getMetaValue($account, 'account_role');
if ('' === $role) {
$role = 'no_account_type';
}
$key = (string) trans(sprintf('firefly.opt_group_%s', $role));
$grouped[$key][$account->id] = sprintf('%s (%s)', $account->name, Amount::formatAnything($currency, $balance, false));
}
return $grouped;
}
/**
* @throws Exception
*/
private function getLiabilities(): array
{
// make repositories
$accountList = $this->accountRepository->getActiveAccountsByType([
AccountTypeEnum::LOAN->value,
AccountTypeEnum::DEBT->value,
AccountTypeEnum::MORTGAGE->value,
]);
$grouped = [];
// group accounts:
/** @var Account $account */
foreach ($accountList as $account) {
$date = today()->endOfDay();
Log::debug(sprintf('getLiabilities: Call finalAccountBalance with date/time "%s"', $date->toIso8601String()));
// 2025-10-08 replace finalAccountBalance with accountsBalancesOptimized.
// $balance = Steam::finalAccountBalance($account, $date)['balance'];
$balance = Steam::accountsBalancesOptimized(new Collection()->push($account), $date)[$account->id]['balance'] ?? '0';
$currency = $this->accountRepository->getAccountCurrency($account) ?? $this->primaryCurrency;
$role = sprintf('l_%s', $account->accountType->type);
$key = (string) trans(sprintf('firefly.opt_group_%s', $role));
$grouped[$key][$account->id] = sprintf('%s (%s)', $account->name, Amount::formatAnything($currency, $balance, false));
}
return $grouped;
}
private function getValidDepositSources(): array
{
// make repositories
$liabilityTypes = [AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::CREDITCARD->value, AccountTypeEnum::LOAN->value];
$accountList = $this->accountRepository->getActiveAccountsByType([
AccountTypeEnum::REVENUE->value,
AccountTypeEnum::CASH->value,
AccountTypeEnum::LOAN->value,
AccountTypeEnum::DEBT->value,
AccountTypeEnum::MORTGAGE->value,
]);
$grouped = [];
// group accounts:
/** @var Account $account */
foreach ($accountList as $account) {
$role = (string) $this->accountRepository->getMetaValue($account, 'account_role');
$name = $account->name;
if ('' === $role) {
$role = 'no_account_type';
}
// maybe it's a liability thing:
if (in_array($account->accountType->type, $liabilityTypes, true)) {
$role = 'l_'.$account->accountType->type;
}
if (AccountTypeEnum::CASH->value === $account->accountType->type) {
$role = 'cash_account';
$name = sprintf('(%s)', trans('firefly.cash'));
}
if (AccountTypeEnum::REVENUE->value === $account->accountType->type) {
$role = 'revenue_account';
}
$key = (string) trans('firefly.opt_group_'.$role);
$grouped[$key][$account->id] = $name;
}
return $grouped;
}
private function getValidWithdrawalDests(): array
{
// make repositories
$liabilityTypes = [AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::CREDITCARD->value, AccountTypeEnum::LOAN->value];
$accountList = $this->accountRepository->getActiveAccountsByType([
AccountTypeEnum::EXPENSE->value,
AccountTypeEnum::CASH->value,
AccountTypeEnum::LOAN->value,
AccountTypeEnum::DEBT->value,
AccountTypeEnum::MORTGAGE->value,
]);
$grouped = [];
// group accounts:
/** @var Account $account */
foreach ($accountList as $account) {
$role = (string) $this->accountRepository->getMetaValue($account, 'account_role');
$name = $account->name;
if ('' === $role) {
$role = 'no_account_type';
}
// maybe it's a liability thing:
if (in_array($account->accountType->type, $liabilityTypes, true)) {
$role = 'l_'.$account->accountType->type;
}
if (AccountTypeEnum::CASH->value === $account->accountType->type) {
$role = 'cash_account';
$name = sprintf('(%s)', trans('firefly.cash'));
}
if (AccountTypeEnum::EXPENSE->value === $account->accountType->type) {
$role = 'expense_account';
}
$key = (string) trans('firefly.opt_group_'.$role);
$grouped[$key][$account->id] = $name;
}
return $grouped;
}
}
@@ -52,7 +52,7 @@ class DeleteController extends Controller
// translations:
$this->middleware(function ($request, $next) {
app('view')->share('title', (string)trans('firefly.transactions'));
app('view')->share('title', (string) trans('firefly.transactions'));
app('view')->share('mainTitleIcon', 'fa-exchange');
$this->repository = app(TransactionGroupRepositoryInterface::class);
@@ -64,7 +64,7 @@ class DeleteController extends Controller
/**
* Shows the form that allows a user to delete a transaction journal.
*/
public function delete(TransactionGroup $group): Factory | Redirector | RedirectResponse | View
public function delete(TransactionGroup $group): Factory|Redirector|RedirectResponse|View
{
if (!$this->isEditableGroup($group)) {
return $this->redirectGroupToAccount($group);
@@ -72,12 +72,12 @@ class DeleteController extends Controller
Log::debug(sprintf('Start of delete view for group #%d', $group->id));
$journal = $group->transactionJournals->first();
$journal = $group->transactionJournals->first();
if (null === $journal) {
throw new NotFoundHttpException();
}
$objectType = strtolower($journal->transaction_type_type ?? $journal->transactionType->type);
$subTitle = (string)trans('firefly.delete_' . $objectType, ['description' => $group->title ?? $journal->description]);
$subTitle = (string) trans('firefly.delete_'.$objectType, ['description' => $group->title ?? $journal->description]);
$previous = Steam::getSafePreviousUrl();
// put previous url in session
Log::debug('Will try to remember previous URL');
@@ -95,19 +95,19 @@ class DeleteController extends Controller
/**
* Actually destroys the journal.
*/
public function destroy(TransactionGroup $group): Redirector | RedirectResponse
public function destroy(TransactionGroup $group): Redirector|RedirectResponse
{
Log::debug(sprintf('Now in %s(#%d).', __METHOD__, $group->id));
if (!$this->isEditableGroup($group)) {
return $this->redirectGroupToAccount($group);
}
$journal = $group->transactionJournals->first();
$journal = $group->transactionJournals->first();
if (null === $journal) {
throw new NotFoundHttpException();
}
$objectType = strtolower($journal->transaction_type_type ?? $journal->transactionType->type);
session()->flash('success', (string)trans('firefly.deleted_' . strtolower($objectType), ['description' => $group->title ?? $journal->description]));
session()->flash('success', (string) trans('firefly.deleted_'.strtolower($objectType), ['description' => $group->title ?? $journal->description]));
$this->repository->destroy($group);
Preferences::mark();
@@ -192,43 +192,6 @@ class MassController extends Controller
return redirect($this->getPreviousUrl('transactions.mass-edit.url'));
}
/**
* @throws FireflyException
*/
private function updateJournal(int $journalId, MassEditJournalRequest $request): void
{
$journal = $this->repository->find($journalId);
if (!$journal instanceof TransactionJournal) {
throw new FireflyException(sprintf('Trying to edit non-existent or deleted journal #%d', $journalId));
}
$service = app(JournalUpdateService::class);
// for each field, call the update service.
$service->setTransactionJournal($journal);
$data = [
'date' => $this->getDateFromRequest($request, $journal->id, 'date'),
'description' => $this->getStringFromRequest($request, $journal->id, 'description'),
'source_id' => $this->getIntFromRequest($request, $journal->id, 'source_id'),
'source_name' => $this->getStringFromRequest($request, $journal->id, 'source_name'),
'destination_id' => $this->getIntFromRequest($request, $journal->id, 'destination_id'),
'destination_name' => $this->getStringFromRequest($request, $journal->id, 'destination_name'),
'budget_id' => $this->getIntFromRequest($request, $journal->id, 'budget_id'),
'category_name' => $this->getStringFromRequest($request, $journal->id, 'category'),
'amount' => $this->getStringFromRequest($request, $journal->id, 'amount'),
'foreign_amount' => $this->getStringFromRequest($request, $journal->id, 'foreign_amount'),
];
Log::debug(sprintf('Will update journal #%d with data.', $journal->id), $data);
// call service to update.
$service->setData($data);
$service->update();
$flags = new TransactionGroupEventFlags();
$objects = TransactionGroupEventObjects::collectFromTransactionGroup($journal->transactionGroup);
event(new UpdatedSingleTransactionGroup($flags, $objects));
event(new WebhookMessagesRequestSending());
}
private function getDateFromRequest(MassEditJournalRequest $request, int $journalId, string $key): ?Carbon
{
$value = $request->get($key);
@@ -251,6 +214,19 @@ class MassController extends Controller
return $carbon;
}
private function getIntFromRequest(MassEditJournalRequest $request, int $journalId, string $string): ?int
{
$value = $request->get($string);
if (!is_array($value)) {
return null;
}
if (!array_key_exists($journalId, $value)) {
return null;
}
return (int) $value[$journalId];
}
private function getStringFromRequest(MassEditJournalRequest $request, int $journalId, string $string): ?string
{
$value = $request->get($string);
@@ -264,16 +240,39 @@ class MassController extends Controller
return (string) $value[$journalId];
}
private function getIntFromRequest(MassEditJournalRequest $request, int $journalId, string $string): ?int
/**
* @throws FireflyException
*/
private function updateJournal(int $journalId, MassEditJournalRequest $request): void
{
$value = $request->get($string);
if (!is_array($value)) {
return null;
}
if (!array_key_exists($journalId, $value)) {
return null;
$journal = $this->repository->find($journalId);
if (!$journal instanceof TransactionJournal) {
throw new FireflyException(sprintf('Trying to edit non-existent or deleted journal #%d', $journalId));
}
$service = app(JournalUpdateService::class);
// for each field, call the update service.
$service->setTransactionJournal($journal);
return (int) $value[$journalId];
$data = [
'date' => $this->getDateFromRequest($request, $journal->id, 'date'),
'description' => $this->getStringFromRequest($request, $journal->id, 'description'),
'source_id' => $this->getIntFromRequest($request, $journal->id, 'source_id'),
'source_name' => $this->getStringFromRequest($request, $journal->id, 'source_name'),
'destination_id' => $this->getIntFromRequest($request, $journal->id, 'destination_id'),
'destination_name' => $this->getStringFromRequest($request, $journal->id, 'destination_name'),
'budget_id' => $this->getIntFromRequest($request, $journal->id, 'budget_id'),
'category_name' => $this->getStringFromRequest($request, $journal->id, 'category'),
'amount' => $this->getStringFromRequest($request, $journal->id, 'amount'),
'foreign_amount' => $this->getStringFromRequest($request, $journal->id, 'foreign_amount'),
];
Log::debug(sprintf('Will update journal #%d with data.', $journal->id), $data);
// call service to update.
$service->setData($data);
$service->update();
$flags = new TransactionGroupEventFlags();
$objects = TransactionGroupEventObjects::collectFromTransactionGroup($journal->transactionGroup);
event(new UpdatedSingleTransactionGroup($flags, $objects));
event(new WebhookMessagesRequestSending());
}
}
@@ -163,6 +163,31 @@ class ShowController extends Controller
]);
}
private function getAccounts(array $group): array
{
$accounts = ['source' => [], 'destination' => []];
foreach ($group['transactions'] as $transaction) {
$accounts['source'][] = [
'type' => $transaction['source_account_type'],
'id' => $transaction['source_account_id'],
'name' => $transaction['source_account_name'],
'iban' => $transaction['source_account_iban'],
];
$accounts['destination'][] = [
'type' => $transaction['destination_account_type'],
'id' => $transaction['destination_account_id'],
'name' => $transaction['destination_account_name'],
'iban' => $transaction['destination_account_iban'],
];
}
$accounts['source'] = array_unique($accounts['source'], SORT_REGULAR);
$accounts['destination'] = array_unique($accounts['destination'], SORT_REGULAR);
return $accounts;
}
private function getAmounts(array $group): array
{
$amounts = [];
@@ -202,29 +227,4 @@ class ShowController extends Controller
return $amounts;
}
private function getAccounts(array $group): array
{
$accounts = ['source' => [], 'destination' => []];
foreach ($group['transactions'] as $transaction) {
$accounts['source'][] = [
'type' => $transaction['source_account_type'],
'id' => $transaction['source_account_id'],
'name' => $transaction['source_account_name'],
'iban' => $transaction['source_account_iban'],
];
$accounts['destination'][] = [
'type' => $transaction['destination_account_type'],
'id' => $transaction['destination_account_id'],
'name' => $transaction['destination_account_name'],
'iban' => $transaction['destination_account_iban'],
];
}
$accounts['source'] = array_unique($accounts['source'], SORT_REGULAR);
$accounts['destination'] = array_unique($accounts['destination'], SORT_REGULAR);
return $accounts;
}
}
+16 -16
View File
@@ -78,6 +78,22 @@ class Installer
return $next($request);
}
/**
* Is access denied error.
*/
protected function isAccessDenied(string $message): bool
{
return false !== stripos($message, 'Access denied');
}
/**
* Is no tables exist error.
*/
protected function noTablesExist(string $message): bool
{
return false !== stripos($message, 'Base table or view not found');
}
/**
* Check if the tables are created and accounted for.
*
@@ -113,20 +129,4 @@ class Installer
return false;
}
/**
* Is access denied error.
*/
protected function isAccessDenied(string $message): bool
{
return false !== stripos($message, 'Access denied');
}
/**
* Is no tables exist error.
*/
protected function noTablesExist(string $message): bool
{
return false !== stripos($message, 'Base table or view not found');
}
}
+118 -118
View File
@@ -82,10 +82,31 @@ class InterestingMessage
return $next($request);
}
private function testing(): bool
private function accountMessage(Request $request): bool
{
// ignore middleware in test environment.
return 'testing' === config('app.env') || !auth()->check();
// get parameters from request.
$accountId = $request->get('account_id');
$message = $request->get('message');
return null !== $accountId && null !== $message;
}
private function billMessage(Request $request): bool
{
// get parameters from request.
$billId = $request->get('bill_id');
$message = $request->get('message');
return null !== $billId && null !== $message;
}
private function currencyMessage(Request $request): bool
{
// get parameters from request.
$code = $request->get('code');
$message = $request->get('message');
return null !== $code && null !== $message;
}
private function groupMessage(Request $request): bool
@@ -97,6 +118,85 @@ class InterestingMessage
return null !== $transactionGroupId && null !== $message;
}
private function handleAccountMessage(Request $request): void
{
// get parameters from request.
$accountId = $request->get('account_id');
$message = $request->get('message');
/** @var User $user */
$user = auth()->user();
/** @var null|Account $account */
$account = $user->accounts()->withTrashed()->find($accountId);
if (null === $account) {
return;
}
if ('deleted' === $message) {
session()->flash('success', (string) trans('firefly.account_deleted', ['name' => $account->name]));
}
if ('created' === $message) {
session()->flash('success', (string) trans('firefly.stored_new_account', ['name' => $account->name]));
}
if ('updated' === $message) {
session()->flash('success', (string) trans('firefly.updated_account', ['name' => $account->name]));
}
}
private function handleBillMessage(Request $request): void
{
// get parameters from request.
$billId = $request->get('bill_id');
$message = $request->get('message');
/** @var null|Bill $bill */
$bill = auth()->user()->bills()->withTrashed()->find($billId);
if (null === $bill) {
return;
}
if ('deleted' === $message) {
session()->flash('success', (string) trans('firefly.deleted_bill', ['name' => $bill->name]));
}
if ('created' === $message) {
session()->flash('success', (string) trans('firefly.stored_new_bill', ['name' => $bill->name]));
}
}
private function handleCurrencyMessage(Request $request): void
{
// params:
// get parameters from request.
$code = (string) $request->get('code');
$message = (string) $request->get('message');
try {
$currency = Amount::getTransactionCurrencyByCode($code);
} catch (FireflyException) {
return;
}
if ('enabled' === $message) {
session()->flash('success', (string) trans('firefly.currency_is_now_enabled', ['name' => $currency->name]));
}
if ('enable_failed' === $message) {
session()->flash('error', (string) trans('firefly.could_not_enable_currency', ['name' => $currency->name]));
}
if ('disabled' === $message) {
session()->flash('success', (string) trans('firefly.currency_is_now_disabled', ['name' => $currency->name]));
}
if ('disable_failed' === $message) {
session()->flash('error', (string) trans('firefly.could_not_disable_currency', ['name' => $currency->name]));
}
if ('default' === $message) {
session()->flash('success', (string) trans('firefly.new_default_currency', ['name' => $currency->name]));
}
if ('default_failed' === $message) {
session()->flash('error', (string) trans('firefly.default_currency_failed', ['name' => $currency->name]));
}
}
private function handleGroupMessage(Request $request): void
{
// get parameters from request.
@@ -135,15 +235,6 @@ class InterestingMessage
}
}
private function userGroupMessage(Request $request): bool
{
// get parameters from request.
$transactionGroupId = $request->get('user_group_id');
$message = $request->get('message');
return null !== $transactionGroupId && null !== $message;
}
private function handleUserGroupMessage(Request $request): void
{
// get parameters from request.
@@ -181,79 +272,6 @@ class InterestingMessage
}
}
private function accountMessage(Request $request): bool
{
// get parameters from request.
$accountId = $request->get('account_id');
$message = $request->get('message');
return null !== $accountId && null !== $message;
}
private function handleAccountMessage(Request $request): void
{
// get parameters from request.
$accountId = $request->get('account_id');
$message = $request->get('message');
/** @var User $user */
$user = auth()->user();
/** @var null|Account $account */
$account = $user->accounts()->withTrashed()->find($accountId);
if (null === $account) {
return;
}
if ('deleted' === $message) {
session()->flash('success', (string) trans('firefly.account_deleted', ['name' => $account->name]));
}
if ('created' === $message) {
session()->flash('success', (string) trans('firefly.stored_new_account', ['name' => $account->name]));
}
if ('updated' === $message) {
session()->flash('success', (string) trans('firefly.updated_account', ['name' => $account->name]));
}
}
private function billMessage(Request $request): bool
{
// get parameters from request.
$billId = $request->get('bill_id');
$message = $request->get('message');
return null !== $billId && null !== $message;
}
private function handleBillMessage(Request $request): void
{
// get parameters from request.
$billId = $request->get('bill_id');
$message = $request->get('message');
/** @var null|Bill $bill */
$bill = auth()->user()->bills()->withTrashed()->find($billId);
if (null === $bill) {
return;
}
if ('deleted' === $message) {
session()->flash('success', (string) trans('firefly.deleted_bill', ['name' => $bill->name]));
}
if ('created' === $message) {
session()->flash('success', (string) trans('firefly.stored_new_bill', ['name' => $bill->name]));
}
}
private function webhookMessage(Request $request): bool
{
// get parameters from request.
$webhookId = $request->get('webhook_id');
$message = $request->get('message');
return null !== $webhookId && null !== $message;
}
private function handleWebhookMessage(Request $request): void
{
// get parameters from request.
@@ -277,45 +295,27 @@ class InterestingMessage
}
}
private function currencyMessage(Request $request): bool
private function testing(): bool
{
// get parameters from request.
$code = $request->get('code');
$message = $request->get('message');
return null !== $code && null !== $message;
// ignore middleware in test environment.
return 'testing' === config('app.env') || !auth()->check();
}
private function handleCurrencyMessage(Request $request): void
private function userGroupMessage(Request $request): bool
{
// params:
// get parameters from request.
$code = (string) $request->get('code');
$message = (string) $request->get('message');
$transactionGroupId = $request->get('user_group_id');
$message = $request->get('message');
try {
$currency = Amount::getTransactionCurrencyByCode($code);
} catch (FireflyException) {
return;
}
return null !== $transactionGroupId && null !== $message;
}
if ('enabled' === $message) {
session()->flash('success', (string) trans('firefly.currency_is_now_enabled', ['name' => $currency->name]));
}
if ('enable_failed' === $message) {
session()->flash('error', (string) trans('firefly.could_not_enable_currency', ['name' => $currency->name]));
}
if ('disabled' === $message) {
session()->flash('success', (string) trans('firefly.currency_is_now_disabled', ['name' => $currency->name]));
}
if ('disable_failed' === $message) {
session()->flash('error', (string) trans('firefly.could_not_disable_currency', ['name' => $currency->name]));
}
if ('default' === $message) {
session()->flash('success', (string) trans('firefly.new_default_currency', ['name' => $currency->name]));
}
if ('default_failed' === $message) {
session()->flash('error', (string) trans('firefly.default_currency_failed', ['name' => $currency->name]));
}
private function webhookMessage(Request $request): bool
{
// get parameters from request.
$webhookId = $request->get('webhook_id');
$message = $request->get('message');
return null !== $webhookId && null !== $message;
}
}
+36 -36
View File
@@ -65,37 +65,17 @@ class Range
}
/**
* Set the range for the current view.
* Configure the list length.
*/
private function setRange(): void
private function configureList(): void
{
// ignore preference. set the range to be the current month:
if (!app('session')->has('start') && !app('session')->has('end')) {
Log::debug('setRange: Session has no start or end.');
$viewRange = Preferences::get('viewRange', '1M')->data;
if (is_array($viewRange)) {
$viewRange = '1M';
}
$pref = Preferences::get('list-length', config('firefly.list_length', 10))->data;
app('view')->share('listLength', $pref);
$today = today(config('app.timezone'));
$start = Navigation::updateStartDate((string) $viewRange, $today);
$end = Navigation::updateEndDate((string) $viewRange, $start);
app('session')->put('start', $start);
app('session')->put('end', $end);
}
if (!app('session')->has('first')) {
Log::debug('setRange: Session has no "first".');
/** @var JournalRepositoryInterface $repository */
$repository = app(JournalRepositoryInterface::class);
$journal = $repository->firstNull();
$first = today(config('app.timezone'))->startOfYear();
if (null !== $journal) {
$first = $journal->date ?? $first;
}
app('session')->put('first', $first);
// share security message:
if (FireflyConfig::has('upgrade_security_message') && FireflyConfig::has('upgrade_security_level')) {
app('view')->share('upgrade_security_message', FireflyConfig::get('upgrade_security_message')->data);
app('view')->share('upgrade_security_level', FireflyConfig::get('upgrade_security_level')->data);
}
}
@@ -136,17 +116,37 @@ class Range
}
/**
* Configure the list length.
* Set the range for the current view.
*/
private function configureList(): void
private function setRange(): void
{
$pref = Preferences::get('list-length', config('firefly.list_length', 10))->data;
app('view')->share('listLength', $pref);
// ignore preference. set the range to be the current month:
if (!app('session')->has('start') && !app('session')->has('end')) {
Log::debug('setRange: Session has no start or end.');
$viewRange = Preferences::get('viewRange', '1M')->data;
if (is_array($viewRange)) {
$viewRange = '1M';
}
// share security message:
if (FireflyConfig::has('upgrade_security_message') && FireflyConfig::has('upgrade_security_level')) {
app('view')->share('upgrade_security_message', FireflyConfig::get('upgrade_security_message')->data);
app('view')->share('upgrade_security_level', FireflyConfig::get('upgrade_security_level')->data);
$today = today(config('app.timezone'));
$start = Navigation::updateStartDate((string) $viewRange, $today);
$end = Navigation::updateEndDate((string) $viewRange, $start);
app('session')->put('start', $start);
app('session')->put('end', $end);
}
if (!app('session')->has('first')) {
Log::debug('setRange: Session has no "first".');
/** @var JournalRepositoryInterface $repository */
$repository = app(JournalRepositoryInterface::class);
$journal = $repository->firstNull();
$first = today(config('app.timezone'))->startOfYear();
if (null !== $journal) {
$first = $journal->date ?? $first;
}
app('session')->put('first', $first);
}
}
}
+43 -43
View File
@@ -147,35 +147,6 @@ class RecurrenceFormRequest extends FormRequest
return $return;
}
/**
* Parses repetition data.
*/
private function parseRepetitionData(): array
{
$value = $this->convertString('repetition_type');
$return = ['type' => '', 'moment' => ''];
if ('daily' === $value) {
$return['type'] = $value;
}
// monthly,17
// ndom,3,7
if (in_array(substr($value, 0, 6), ['yearly', 'weekly'], true)) {
$return['type'] = substr($value, 0, 6);
$return['moment'] = substr($value, 7);
}
if (str_starts_with($value, 'monthly')) {
$return['type'] = substr($value, 0, 7);
$return['moment'] = substr($value, 8);
}
if (str_starts_with($value, 'ndom')) {
$return['type'] = substr($value, 0, 4);
$return['moment'] = substr($value, 5);
}
return $return;
}
/**
* The rules for this request.
*/
@@ -262,20 +233,6 @@ class RecurrenceFormRequest extends FormRequest
return $rules;
}
/**
* Configure the validator instance with special rules for after the basic validation rules.
*/
public function withValidator(Validator $validator): void
{
$validator->after(function (Validator $validator): void {
// validate all account info
$this->validateAccountInformation($validator);
});
if ($validator->fails()) {
Log::channel('audit')->error(sprintf('Validation errors in %s', self::class), $validator->errors()->toArray());
}
}
/**
* Validates the given account information. Switches on given transaction type.
*
@@ -340,4 +297,47 @@ class RecurrenceFormRequest extends FormRequest
$validator->errors()->add('withdrawal_destination_id', $message);
}
}
/**
* Configure the validator instance with special rules for after the basic validation rules.
*/
public function withValidator(Validator $validator): void
{
$validator->after(function (Validator $validator): void {
// validate all account info
$this->validateAccountInformation($validator);
});
if ($validator->fails()) {
Log::channel('audit')->error(sprintf('Validation errors in %s', self::class), $validator->errors()->toArray());
}
}
/**
* Parses repetition data.
*/
private function parseRepetitionData(): array
{
$value = $this->convertString('repetition_type');
$return = ['type' => '', 'moment' => ''];
if ('daily' === $value) {
$return['type'] = $value;
}
// monthly,17
// ndom,3,7
if (in_array(substr($value, 0, 6), ['yearly', 'weekly'], true)) {
$return['type'] = substr($value, 0, 6);
$return['moment'] = substr($value, 7);
}
if (str_starts_with($value, 'monthly')) {
$return['type'] = substr($value, 0, 7);
$return['moment'] = substr($value, 8);
}
if (str_starts_with($value, 'ndom')) {
$return['type'] = substr($value, 0, 4);
$return['moment'] = substr($value, 5);
}
return $return;
}
}
+56 -56
View File
@@ -41,47 +41,6 @@ class RuleFormRequest extends FormRequest
use ConvertsDataTypes;
use GetRuleConfiguration;
/**
* Get all data for controller.
*/
public function getRuleData(): array
{
return [
'title' => $this->convertString('title'),
'rule_group_id' => $this->convertInteger('rule_group_id'),
'active' => $this->boolean('active'),
'trigger' => $this->convertString('trigger'),
'description' => $this->stringWithNewlines('description'),
'stop_processing' => $this->boolean('stop_processing'),
'strict' => $this->boolean('strict'),
'run_after_form' => $this->boolean('run_after_form'),
'triggers' => $this->getRuleTriggerData(),
'actions' => $this->getRuleActionData(),
];
}
private function getRuleTriggerData(): array
{
$return = [];
$triggerData = $this->get('triggers');
if (is_array($triggerData)) {
foreach ($triggerData as $trigger) {
$stopProcessing = $trigger['stop_processing'] ?? '0';
$prohibited = $trigger['prohibited'] ?? '0';
$set = [
'type' => $trigger['type'] ?? 'invalid',
'value' => $trigger['value'] ?? '',
'stop_processing' => 1 === (int) $stopProcessing,
'prohibited' => 1 === (int) $prohibited,
];
$set = self::replaceAmountTrigger($set);
$return[] = $set;
}
}
return $return;
}
public static function replaceAmountTrigger(array $array): array
{
// do some sneaky search and replace.
@@ -107,22 +66,23 @@ class RuleFormRequest extends FormRequest
return $array;
}
private function getRuleActionData(): array
/**
* Get all data for controller.
*/
public function getRuleData(): array
{
$return = [];
$actionData = $this->get('actions');
if (is_array($actionData)) {
foreach ($actionData as $action) {
$stopProcessing = $action['stop_processing'] ?? '0';
$return[] = [
'type' => $action['type'] ?? 'invalid',
'value' => $action['value'] ?? '',
'stop_processing' => 1 === (int) $stopProcessing,
];
}
}
return $return;
return [
'title' => $this->convertString('title'),
'rule_group_id' => $this->convertInteger('rule_group_id'),
'active' => $this->boolean('active'),
'trigger' => $this->convertString('trigger'),
'description' => $this->stringWithNewlines('description'),
'stop_processing' => $this->boolean('stop_processing'),
'strict' => $this->boolean('strict'),
'run_after_form' => $this->boolean('run_after_form'),
'triggers' => $this->getRuleTriggerData(),
'actions' => $this->getRuleActionData(),
];
}
/**
@@ -170,4 +130,44 @@ class RuleFormRequest extends FormRequest
Log::channel('audit')->error(sprintf('Validation errors in %s', self::class), $validator->errors()->toArray());
}
}
private function getRuleActionData(): array
{
$return = [];
$actionData = $this->get('actions');
if (is_array($actionData)) {
foreach ($actionData as $action) {
$stopProcessing = $action['stop_processing'] ?? '0';
$return[] = [
'type' => $action['type'] ?? 'invalid',
'value' => $action['value'] ?? '',
'stop_processing' => 1 === (int) $stopProcessing,
];
}
}
return $return;
}
private function getRuleTriggerData(): array
{
$return = [];
$triggerData = $this->get('triggers');
if (is_array($triggerData)) {
foreach ($triggerData as $trigger) {
$stopProcessing = $trigger['stop_processing'] ?? '0';
$prohibited = $trigger['prohibited'] ?? '0';
$set = [
'type' => $trigger['type'] ?? 'invalid',
'value' => $trigger['value'] ?? '',
'stop_processing' => 1 === (int) $stopProcessing,
'prohibited' => 1 === (int) $prohibited,
];
$set = self::replaceAmountTrigger($set);
$return[] = $set;
}
}
return $return;
}
}