Merge pull request #11946 from firefly-iii/release-1773474241

🤖 Automatically merge the PR into the develop branch.
This commit is contained in:
github-actions[bot]
2026-03-14 08:44:10 +01:00
committed by GitHub
22 changed files with 136 additions and 61 deletions
@@ -46,7 +46,7 @@ final class DestroyController extends Controller
parent::__construct();
$this->middleware(function ($request, $next) {
/** @var User $user */
$user = auth()->user();
$user = auth()->user();
$this->repository = app(LinkTypeRepositoryInterface::class);
@@ -43,7 +43,7 @@ final class StoreController extends Controller
{
use TransactionFilter;
private JournalRepositoryInterface $journalRepository;
private JournalRepositoryInterface $journalRepository;
private LinkTypeRepositoryInterface $repository;
/**
@@ -54,7 +54,7 @@ final class StoreController extends Controller
parent::__construct();
$this->middleware(function ($request, $next) {
/** @var User $user */
$user = auth()->user();
$user = auth()->user();
$this->repository = app(LinkTypeRepositoryInterface::class);
$this->journalRepository = app(JournalRepositoryInterface::class);
@@ -76,22 +76,22 @@ final class StoreController extends Controller
*/
public function store(StoreRequest $request): JsonResponse
{
$manager = $this->getManager();
$data = $request->getAll();
$inward = $this->journalRepository->find($data['inward_id'] ?? 0);
$outward = $this->journalRepository->find($data['outward_id'] ?? 0);
$manager = $this->getManager();
$data = $request->getAll();
$inward = $this->journalRepository->find($data['inward_id'] ?? 0);
$outward = $this->journalRepository->find($data['outward_id'] ?? 0);
if (!$inward instanceof TransactionJournal || !$outward instanceof TransactionJournal) {
throw new FireflyException('200024: Source or destination does not exist.');
}
$data['direction'] = 'inward';
$journalLink = $this->repository->storeLink($data, $inward, $outward);
$journalLink = $this->repository->storeLink($data, $inward, $outward);
/** @var TransactionLinkTransformer $transformer */
$transformer = app(TransactionLinkTransformer::class);
$transformer = app(TransactionLinkTransformer::class);
$transformer->setParameters($this->parameters);
$resource = new Item($journalLink, $transformer, 'transaction_links');
$resource = new Item($journalLink, $transformer, 'transaction_links');
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);
}
@@ -39,7 +39,7 @@ use League\Fractal\Resource\Item;
*/
final class UpdateController extends Controller
{
private JournalRepositoryInterface $journalRepository;
private JournalRepositoryInterface $journalRepository;
private LinkTypeRepositoryInterface $repository;
/**
@@ -50,7 +50,7 @@ final class UpdateController extends Controller
parent::__construct();
$this->middleware(function ($request, $next) {
/** @var User $user */
$user = auth()->user();
$user = auth()->user();
$this->repository = app(LinkTypeRepositoryInterface::class);
$this->journalRepository = app(JournalRepositoryInterface::class);
@@ -78,7 +78,7 @@ final class UpdateController extends Controller
$transformer = app(TransactionLinkTransformer::class);
$transformer->setParameters($this->parameters);
$resource = new Item($journalLink, $transformer, 'transaction_links');
$resource = new Item($journalLink, $transformer, 'transaction_links');
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);
}
@@ -98,9 +98,10 @@ class RemovesLinksToDeletedObjects extends Command
// count and clean up available budgets in currencies with no budget limits.
// this is not entirely the place for it but OK.
/** @var AvailableBudgetRepositoryInterface $repository */
$repository = app(AvailableBudgetRepositoryInterface::class);
$repository = app(AvailableBudgetRepositoryInterface::class);
/** @var User $user */
foreach(User::get() as $user) {
foreach (User::get() as $user) {
$repository->setUser($user);
$repository->cleanup();
}
@@ -75,7 +75,10 @@ class RollbacksSingleMigration extends Command
}
if ($res) {
DB::table('migrations')->where('id', (int) $entry->id)->delete();
DB::table('migrations')
->where('id', (int) $entry->id)
->delete()
;
$this->friendlyInfo(sprintf('Database migration #%d ("%s") is deleted.', $entry->id, $entry->migration));
$this->friendlyLine('');
$this->friendlyLine('Try running "php artisan migrate" now.');
@@ -107,7 +107,10 @@ class RepairsPostgresSequences extends Command
$this->friendlyLine(sprintf('Checking the next id sequence for table "%s".', $tableToCheck));
$highestId = DB::table($tableToCheck)->select(DB::raw('MAX(id)'))->first();
$nextId = DB::table($tableToCheck)->select(DB::raw(sprintf('nextval(\'%s_id_seq\')', $tableToCheck)))->first();
$nextId = DB::table($tableToCheck)
->select(DB::raw(sprintf('nextval(\'%s_id_seq\')', $tableToCheck)))
->first()
;
if (null === $nextId) {
$this->friendlyInfo(sprintf('nextval is NULL for table "%s", go to next table.', $tableToCheck));
@@ -117,7 +120,10 @@ class RepairsPostgresSequences extends Command
if ($nextId->nextval < $highestId->max) {
DB::select(sprintf('SELECT setval(\'%s_id_seq\', %d)', $tableToCheck, $highestId->max));
$highestId = DB::table($tableToCheck)->select(DB::raw('MAX(id)'))->first();
$nextId = DB::table($tableToCheck)->select(DB::raw(sprintf('nextval(\'%s_id_seq\')', $tableToCheck)))->first();
$nextId = DB::table($tableToCheck)
->select(DB::raw(sprintf('nextval(\'%s_id_seq\')', $tableToCheck)))
->first()
;
if ($nextId->nextval > $highestId->max) {
$this->friendlyInfo(sprintf('Table "%s" autoincrement corrected.', $tableToCheck));
}
+6 -1
View File
@@ -205,7 +205,12 @@ class InterestingMessage
// send message about newly created transaction group.
/** @var null|TransactionGroup $group */
$group = auth()->user()->transactionGroups()->with(['transactionJournals', 'transactionJournals.transactionType'])->find((int) $transactionGroupId);
$group = auth()
->user()
->transactionGroups()
->with(['transactionJournals', 'transactionJournals.transactionType'])
->find((int) $transactionGroupId)
;
if (null === $group) {
return;
+1 -1
View File
@@ -74,7 +74,7 @@ class SecureHeaders
"default-src 'none'",
"object-src 'none'",
sprintf("script-src 'unsafe-eval' 'strict-dynamic' 'nonce-%1s'", $nonce),
// sprintf("style-src 'self' 'nonce-%1s' https://10.0.0.15:5173/", $nonce), // safe variant
// sprintf("style-src 'self' 'nonce-%1s' https://10.0.0.15:5173/", $nonce), // safe variant
"style-src 'self' 'unsafe-inline' https://10.0.0.15:5173/", // unsafe variant
"base-uri 'self'",
"form-action 'self'",
+5 -1
View File
@@ -73,7 +73,11 @@ class Preference extends Model
// try again with ID, but this time don't care about the preferred user_group_id
if (null === $preference) {
$preference = $user->preferences()->where('id', (int) $value)->first();
$preference = $user
->preferences()
->where('id', (int) $value)
->first()
;
}
if (null !== $preference) {
/** @var Preference $preference */
@@ -58,19 +58,32 @@ class AvailableBudgetRepository implements AvailableBudgetRepositoryInterface, U
$end = $availableBudget->end_date->format('Y-m-d');
$key = sprintf('%s-%s-%s', $availableBudget->transaction_currency_id, $start, $end);
if (array_key_exists($key, $exists)) {
Log::debug(sprintf('Found duplicate AB: %s %s, %s-%s. Has been deleted', $availableBudget->transaction_currency_id, $availableBudget->amount, $start, $end));
Log::debug(sprintf(
'Found duplicate AB: %s %s, %s-%s. Has been deleted',
$availableBudget->transaction_currency_id,
$availableBudget->amount,
$start,
$end
));
$availableBudget->delete();
}
$exists[$key] = true;
}
// grab budget limit currencies.
$currencies = BudgetLimit
::leftJoin('budgets','budgets.id','=','budget_limits.budget_id')
$currencies = BudgetLimit::leftJoin('budgets', 'budgets.id', '=', 'budget_limits.budget_id')
->where('budgets.user_id', $this->user->id)
->distinct()->get(['budget_limits.transaction_currency_id'])->pluck('transaction_currency_id')->toArray();
->distinct()
->get(['budget_limits.transaction_currency_id'])
->pluck('transaction_currency_id')
->toArray()
;
// delete available budgets without these currencies.
$this->user->availableBudgets()->whereNotIn('transaction_currency_id', $currencies)->delete();
$this->user
->availableBudgets()
->whereNotIn('transaction_currency_id', $currencies)
->delete()
;
}
/**
+8 -2
View File
@@ -256,8 +256,14 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface
foreach ($budgets as $budget) {
DB::table('budget_transaction')->where('budget_id', $budget->id)->delete();
DB::table('budget_transaction_journal')->where('budget_id', $budget->id)->delete();
RecurrenceTransactionMeta::where('name', 'budget_id')->where('value', (string) $budget->id)->delete();
RuleAction::where('action_type', 'set_budget')->where('action_value', (string) $budget->id)->delete();
RecurrenceTransactionMeta::where('name', 'budget_id')
->where('value', (string) $budget->id)
->delete()
;
RuleAction::where('action_type', 'set_budget')
->where('action_value', (string) $budget->id)
->delete()
;
$budget->delete();
}
Log::channel('audit')->info('Delete all budgets through destroyAll');
@@ -86,7 +86,10 @@ class CurrencyRepository implements CurrencyRepositoryInterface, UserGroupInterf
}
// is being used in accounts:
$meta = AccountMeta::where('name', 'currency_id')->where('data', json_encode((string) $currency->id))->count();
$meta = AccountMeta::where('name', 'currency_id')
->where('data', json_encode((string) $currency->id))
->count()
;
if ($meta > 0) {
Log::info(sprintf('Used in %d accounts as currency_id, return true. ', $meta));
@@ -94,7 +97,10 @@ class CurrencyRepository implements CurrencyRepositoryInterface, UserGroupInterf
}
// second search using integer check.
$meta = AccountMeta::where('name', 'currency_id')->where('data', json_encode((int) $currency->id))->count();
$meta = AccountMeta::where('name', 'currency_id')
->where('data', json_encode((int) $currency->id))
->count()
;
if ($meta > 0) {
Log::info(sprintf('Used in %d accounts as currency_id, return true. ', $meta));
@@ -97,9 +97,10 @@ class LinkTypeRepository implements LinkTypeRepositoryInterface, UserGroupInterf
public function findSpecificLink(LinkType $linkType, TransactionJournal $inward, TransactionJournal $outward): ?TransactionJournalLink
{
return TransactionJournalLink::where('link_type_id', $linkType->id)
->where('source_id', $inward->id)
->where('destination_id', $outward->id)
->first();
->where('source_id', $inward->id)
->where('destination_id', $outward->id)
->first()
;
}
public function get(): Collection
@@ -116,7 +117,14 @@ class LinkTypeRepository implements LinkTypeRepositoryInterface, UserGroupInterf
$sources = $links->pluck('source_id')->toArray();
$destinations = $links->pluck('destination_id')->toArray();
$joined = array_unique(array_merge($sources, $destinations));
return $this->user->transactionJournals()->whereIn('id', $joined)->get(['transaction_journals.id'])->pluck('id')->toArray();
return $this->user
->transactionJournals()
->whereIn('id', $joined)
->get(['transaction_journals.id'])
->pluck('id')
->toArray()
;
}
/**
@@ -125,12 +133,13 @@ class LinkTypeRepository implements LinkTypeRepositoryInterface, UserGroupInterf
public function getJournalLinks(?LinkType $linkType = null): Collection
{
$query = TransactionJournalLink::with(['source', 'destination'])
->leftJoin('transaction_journals as source_journals', 'journal_links.source_id', '=', 'source_journals.id')
->leftJoin('transaction_journals as dest_journals', 'journal_links.destination_id', '=', 'dest_journals.id')
->where('source_journals.user_id', $this->user->id)
->where('dest_journals.user_id', $this->user->id)
->whereNull('source_journals.deleted_at')
->whereNull('dest_journals.deleted_at');
->leftJoin('transaction_journals as source_journals', 'journal_links.source_id', '=', 'source_journals.id')
->leftJoin('transaction_journals as dest_journals', 'journal_links.destination_id', '=', 'dest_journals.id')
->where('source_journals.user_id', $this->user->id)
->where('dest_journals.user_id', $this->user->id)
->whereNull('source_journals.deleted_at')
->whereNull('dest_journals.deleted_at')
;
if ($linkType instanceof LinkType) {
$query->where('journal_links.link_type_id', $linkType->id);
@@ -158,7 +167,7 @@ class LinkTypeRepository implements LinkTypeRepositoryInterface, UserGroupInterf
$inward = TransactionJournalLink::whereDestinationId($journal->id)->get();
$merged = $outward->merge($inward);
return $merged->filter(static fn(TransactionJournalLink $link): bool => null !== $link->source && null !== $link->destination);
return $merged->filter(static fn (TransactionJournalLink $link): bool => null !== $link->source && null !== $link->destination);
}
public function store(array $data): LinkType
@@ -180,7 +189,7 @@ class LinkTypeRepository implements LinkTypeRepositoryInterface, UserGroupInterf
*/
public function storeLink(array $information, TransactionJournal $inward, TransactionJournal $outward): ?TransactionJournalLink
{
$linkType = $this->find((int)($information['link_type_id'] ?? 0));
$linkType = $this->find((int) ($information['link_type_id'] ?? 0));
if (!$linkType instanceof LinkType) {
$linkType = $this->findByName($information['link_type_name']);
@@ -196,7 +205,7 @@ class LinkTypeRepository implements LinkTypeRepositoryInterface, UserGroupInterf
return $existing;
}
$link = new TransactionJournalLink();
$link = new TransactionJournalLink();
$link->linkType()->associate($linkType);
if ('inward' === $information['direction']) {
Log::debug(sprintf('Link type is inwards ("%s"), so %d is source and %d is destination.', $linkType->inward, $inward->id, $outward->id));
@@ -212,7 +221,7 @@ class LinkTypeRepository implements LinkTypeRepositoryInterface, UserGroupInterf
$link->save();
// make note in noteable:
$this->setNoteText($link, (string)$information['notes']);
$this->setNoteText($link, (string) $information['notes']);
return $link;
}
@@ -240,13 +249,13 @@ class LinkTypeRepository implements LinkTypeRepositoryInterface, UserGroupInterf
public function update(LinkType $linkType, array $data): LinkType
{
if (array_key_exists('name', $data) && '' !== (string)$data['name']) {
if (array_key_exists('name', $data) && '' !== (string) $data['name']) {
$linkType->name = $data['name'];
}
if (array_key_exists('inward', $data) && '' !== (string)$data['inward']) {
if (array_key_exists('inward', $data) && '' !== (string) $data['inward']) {
$linkType->inward = $data['inward'];
}
if (array_key_exists('outward', $data) && '' !== (string)$data['outward']) {
if (array_key_exists('outward', $data) && '' !== (string) $data['outward']) {
$linkType->outward = $data['outward'];
}
$linkType->save();
@@ -273,7 +282,7 @@ class LinkTypeRepository implements LinkTypeRepositoryInterface, UserGroupInterf
$journalLink->refresh();
}
$journalLink->link_type_id = $data['link_type_id'] ?? $journalLink->link_type_id;
$journalLink->link_type_id = $data['link_type_id'] ?? $journalLink->link_type_id;
$journalLink->save();
if (array_key_exists('notes', $data) && null !== $data['notes']) {
$this->setNoteText($journalLink, $data['notes']);
+4 -1
View File
@@ -41,7 +41,10 @@ class UserGroupAccount implements BinderInterface
if (auth()->check()) {
/** @var User $user */
$user = auth()->user();
$account = Account::where('id', (int) $value)->where('user_group_id', $user->user_group_id)->first();
$account = Account::where('id', (int) $value)
->where('user_group_id', $user->user_group_id)
->first()
;
if (null !== $account) {
return $account;
}
+4 -1
View File
@@ -41,7 +41,10 @@ class UserGroupBill implements BinderInterface
if (auth()->check()) {
/** @var User $user */
$user = auth()->user();
$currency = Bill::where('id', (int) $value)->where('user_group_id', $user->user_group_id)->first();
$currency = Bill::where('id', (int) $value)
->where('user_group_id', $user->user_group_id)
->first()
;
if (null !== $currency) {
return $currency;
}
+4 -1
View File
@@ -38,7 +38,10 @@ class UserGroupExchangeRate implements BinderInterface
if (auth()->check()) {
/** @var User $user */
$user = auth()->user();
$rate = CurrencyExchangeRate::where('id', (int) $value)->where('user_group_id', $user->user_group_id)->first();
$rate = CurrencyExchangeRate::where('id', (int) $value)
->where('user_group_id', $user->user_group_id)
->first()
;
if (null !== $rate) {
return $rate;
}
+4 -1
View File
@@ -38,7 +38,10 @@ class UserGroupTransaction implements BinderInterface
if (auth()->check()) {
/** @var User $user */
$user = auth()->user();
$group = TransactionGroup::where('id', (int) $value)->where('user_group_id', $user->user_group_id)->first();
$group = TransactionGroup::where('id', (int) $value)
->where('user_group_id', $user->user_group_id)
->first()
;
if (null !== $group) {
return $group;
}
+4 -1
View File
@@ -48,7 +48,10 @@ class AppendNotes implements ActionInterface
public function actOnArray(array $journal): bool
{
$this->refreshNotes($journal);
$dbNote = Note::where('noteable_id', (int) $journal['transaction_journal_id'])->where('noteable_type', TransactionJournal::class)->first(['notes.*']);
$dbNote = Note::where('noteable_id', (int) $journal['transaction_journal_id'])
->where('noteable_type', TransactionJournal::class)
->first(['notes.*'])
;
if (null === $dbNote) {
$dbNote = new Note();
$dbNote->noteable_id = (int) $journal['transaction_journal_id'];
@@ -44,7 +44,10 @@ class PrependNotes implements ActionInterface
public function actOnArray(array $journal): bool
{
$dbNote = Note::where('noteable_id', (int) $journal['transaction_journal_id'])->where('noteable_type', TransactionJournal::class)->first(['notes.*']);
$dbNote = Note::where('noteable_id', (int) $journal['transaction_journal_id'])
->where('noteable_type', TransactionJournal::class)
->first(['notes.*'])
;
if (null === $dbNote) {
$dbNote = new Note();
$dbNote->noteable_id = (int) $journal['transaction_journal_id'];
+5 -1
View File
@@ -355,7 +355,11 @@ trait RecurrenceValidation
}
if (array_key_exists('id', $transaction)) { // don't matter if $idsMandatory
Log::debug('Array has ID.');
$idCount = $recurrence->recurrenceTransactions()->where('recurrences_transactions.id', (int) $transaction['id'])->count();
$idCount = $recurrence
->recurrenceTransactions()
->where('recurrences_transactions.id', (int) $transaction['id'])
->count()
;
if (0 === $idCount) {
Log::debug('ID does not exist or no match. Count another unmatched ID.');
++$unmatchedIds;
+4 -4
View File
@@ -17,10 +17,10 @@ This project adheres to [Semantic Versioning](http://semver.org/).
### Fixed
- #11879
- #11916
- #11936
- #11944
- [Discussion 11879](https://github.com/orgs/firefly-iii/discussions/11879) (Searching for accounts should include inactive accounts?) started by @b-ryan
- [Issue 11916](https://github.com/firefly-iii/firefly-iii/issues/11916) (Balance is not recalculated when multiple transactions are selected and then deleted) reported by @elp3dr0
- [Discussion 11936](https://github.com/orgs/firefly-iii/discussions/11936) (Links in emails don't link to correct domain) started by @SamLMB
- [Issue 11944](https://github.com/firefly-iii/firefly-iii/issues/11944) (Stale available_budgets rows prevent disabling a currency after switching default) reported by @k-leveller
### Security
+2 -2
View File
@@ -78,8 +78,8 @@ return [
'running_balance_column' => (bool)envDefaultWhenEmpty(env('USE_RUNNING_BALANCE'), true), // this is only the default value, is not used.
// see cer.php for exchange rates feature flag.
],
'version' => 'develop/2026-03-13',
'build_time' => 1773385699,
'version' => 'develop/2026-03-14',
'build_time' => 1773474042,
'api_version' => '2.1.0', // field is no longer used.
'db_version' => 28, // field is no longer used.