Add budget transformer and enrichment.

This commit is contained in:
James Cole
2025-08-03 20:17:50 +02:00
parent e55fc483bd
commit 6a49918707
12 changed files with 249 additions and 70 deletions

View File

@@ -29,7 +29,9 @@ use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Budget;
use FireflyIII\Repositories\Budget\BudgetLimitRepositoryInterface;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Support\JsonApi\Enrichments\BudgetEnrichment;
use FireflyIII\Transformers\BudgetTransformer;
use FireflyIII\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Pagination\LengthAwarePaginator;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
@@ -82,6 +84,15 @@ class ShowController extends Controller
$count = $collection->count();
$budgets = $collection->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize);
// enrich
/** @var User $admin */
$admin = auth()->user();
$enrichment = new BudgetEnrichment();
$enrichment->setUser($admin);
$enrichment->setStart($this->parameters->get('start'));
$enrichment->setEnd($this->parameters->get('end'));
$budgets = $enrichment->enrich($budgets);
// make paginator:
$paginator = new LengthAwarePaginator($budgets, $count, $pageSize, $this->parameters->get('page'));
$paginator->setPath(route('api.v1.budgets.index').$this->buildParams());
@@ -103,6 +114,15 @@ class ShowController extends Controller
{
$manager = $this->getManager();
// enrich
/** @var User $admin */
$admin = auth()->user();
$enrichment = new BudgetEnrichment();
$enrichment->setUser($admin);
$enrichment->setStart($this->parameters->get('start'));
$enrichment->setEnd($this->parameters->get('end'));
$budget = $enrichment->enrichSingle($budget);
/** @var BudgetTransformer $transformer */
$transformer = app(BudgetTransformer::class);
$transformer->setParameters($this->parameters);

View File

@@ -95,6 +95,7 @@ class EditController extends Controller
$preFilled = [
'active' => $hasOldInput ? (bool) $request->old('active') : $budget->active,
'auto_budget_currency_id' => $hasOldInput ? (int) $request->old('auto_budget_currency_id') : $this->primaryCurrency->id,
'notes' => $this->repository->getNoteText($budget),
];
if ($autoBudget instanceof AutoBudget) {
$amount = $hasOldInput ? $request->old('auto_budget_amount') : $autoBudget->amount;

View File

@@ -53,6 +53,7 @@ class BudgetFormUpdateRequest extends FormRequest
'currency_id' => $this->convertInteger('auto_budget_currency_id'),
'auto_budget_amount' => $this->convertString('auto_budget_amount'),
'auto_budget_period' => $this->convertString('auto_budget_period'),
'notes' => $this->stringWithNewlines('notes'),
];
}

View File

@@ -290,7 +290,7 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn
$summarizer = new TransactionSummarizer($this->user);
$summarizer->setConvertToPrimary($convertToPrimary);
// filter $journals by range.
// filter $journals by range AND currency if it is present.
$expenses = array_filter($expenses, static function (array $expense) use ($start, $end, $transactionCurrency): bool {
return $expense['date']->between($start, $end) && $expense['currency_id'] === $transactionCurrency->id;
});
@@ -298,6 +298,20 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn
return $summarizer->groupByCurrencyId($expenses, 'negative', false);
}
public function sumCollectedExpensesByBudget(array $expenses, Budget $budget, bool $convertToPrimary = false): array
{
Log::debug(sprintf('Start of %s.', __METHOD__));
$summarizer = new TransactionSummarizer($this->user);
$summarizer->setConvertToPrimary($convertToPrimary);
// filter $journals by range AND currency if it is present.
$expenses = array_filter($expenses, static function (array $expense) use ($budget): bool {
return $expense['budget_id'] === $budget->id;
});
return $summarizer->groupByCurrencyId($expenses, 'negative', false);
}
#[Override]
public function collectExpenses(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $budgets = null, ?TransactionCurrency $currency = null): array
{

View File

@@ -75,6 +75,7 @@ interface OperationsRepositoryInterface
): array;
public function sumCollectedExpenses(array $expenses, Carbon $start, Carbon $end, TransactionCurrency $transactionCurrency, bool $convertToPrimary = false): array;
public function sumCollectedExpensesByBudget(array $expenses, Budget $budget, bool $convertToPrimary = false): array;
public function collectExpenses(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $budgets = null, ?TransactionCurrency $currency = null): array;
}

View File

@@ -0,0 +1,154 @@
<?php
namespace FireflyIII\Support\JsonApi\Enrichments;
use Carbon\Carbon;
use FireflyIII\Models\AutoBudget;
use FireflyIII\Models\Budget;
use FireflyIII\Models\Note;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Models\UserGroup;
use FireflyIII\Repositories\Budget\OperationsRepositoryInterface;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
class BudgetEnrichment implements EnrichmentInterface
{
private Collection $collection;
private bool $convertToPrimary = true;
private TransactionCurrency $primaryCurrency;
private User $user;
private UserGroup $userGroup;
private array $ids = [];
private array $notes = [];
private array $autoBudgets = [];
private array $currencies = [];
private ?Carbon $start = null;
private ?Carbon $end = null;
private array $spent = [];
private array $pcSpent = [];
public function __construct()
{
$this->convertToPrimary = Amount::convertToPrimary();
$this->primaryCurrency = Amount::getPrimaryCurrency();
}
public function enrich(Collection $collection): Collection
{
$this->collection = $collection;
$this->collectIds();
$this->collectNotes();
$this->collectAutoBudgets();
$this->collectExpenses();
$this->appendCollectedData();
return $this->collection;
}
public function enrichSingle(Model|array $model): array|Model
{
Log::debug(__METHOD__);
$collection = new Collection([$model]);
$collection = $this->enrich($collection);
return $collection->first();
}
public function setUser(User $user): void
{
$this->user = $user;
$this->setUserGroup($user->userGroup);
}
public function setUserGroup(UserGroup $userGroup): void
{
$this->userGroup = $userGroup;
}
private function collectIds(): void
{
/** @var Budget $budget */
foreach ($this->collection as $budget) {
$this->ids[] = (int)$budget->id;
}
}
private function collectNotes(): void
{
$notes = Note::query()->whereIn('noteable_id', $this->ids)
->whereNotNull('notes.text')
->where('notes.text', '!=', '')
->where('noteable_type', Budget::class)->get(['notes.noteable_id', 'notes.text'])->toArray();
foreach ($notes as $note) {
$this->notes[(int)$note['noteable_id']] = (string)$note['text'];
}
Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
}
private function appendCollectedData(): void
{
$this->collection = $this->collection->map(function (Budget $item) {
$id = (int)$item->id;
$meta = [
'notes' => $this->notes[$id] ?? null,
'currency' => $this->currencies[$id] ?? null,
'auto_budget' => $this->autoBudgets[$id] ?? null,
'spent' => $this->spent[$id] ?? null,
'pc_spent' => $this->pcSpent[$id] ?? null,
];
$item->meta = $meta;
return $item;
});
}
private function collectAutoBudgets(): void
{
$set = AutoBudget::whereIn('budget_id', $this->ids)->with(['transactionCurrency'])->get();
/** @var AutoBudget $autoBudget */
foreach ($set as $autoBudget) {
$budgetId = (int)$autoBudget->budget_id;
$this->currencies[$budgetId] = $autoBudget->transactionCurrency;
$this->autoBudgets[$budgetId] = [
'type' => (int)$autoBudget->auto_budget_type,
'period' => $autoBudget->period,
'amount' => $autoBudget->amount,
'pc_amount' => $autoBudget->native_amount,
];
}
}
private function collectExpenses(): void
{
if (null !== $this->start && null !== $this->end) {
/** @var OperationsRepositoryInterface $opsRepository */
$opsRepository = app(OperationsRepositoryInterface::class);
$opsRepository->setUser($this->user);
$opsRepository->setUserGroup($this->userGroup);
// $spent = $this->beautify();
// $set = $this->opsRepository->sumExpenses($start, $end, null, new Collection([$budget]))
$expenses = $opsRepository->collectExpenses($this->start, $this->end, null, $this->collection, null);
foreach ($this->collection as $item) {
$id = (int)$item->id;
$this->spent[$id] = array_values($opsRepository->sumCollectedExpensesByBudget($expenses, $item, false));
$this->pcSpent[$id] = array_values($opsRepository->sumCollectedExpensesByBudget($expenses, $item, true));
}
}
}
public function setEnd(?Carbon $end): void
{
$this->end = $end;
}
public function setStart(?Carbon $start): void
{
$this->start = $start;
}
}

View File

@@ -123,7 +123,7 @@ class BudgetLimitEnrichment implements EnrichmentInterface
$this->pcExpenses[$id] = array_values($pcFilteredExpenses);
}
if (true === $this->convertToPrimary && $budgetLimit->transactionCurrency->id === $this->primaryCurrency->id) {
$this->pcExpenses[$id] = $this->expenses[$id];
$this->pcExpenses[$id] = $this->expenses[$id] ?? [];
}
}
}

View File

@@ -113,10 +113,13 @@ class AccountTransformer extends AbstractTransformer
// currency is object specific or primary, already determined above.
'currency_id' => (string) $currency['id'],
'currency_name' => $currency['name'],
'currency_code' => $currency['code'],
'currency_symbol' => $currency['symbol'],
'currency_decimal_places' => $currency['decimal_places'],
'primary_currency_id' => (string) $this->primary->id,
'primary_currency_name' => $this->primary->name,
'primary_currency_code' => $this->primary->code,
'primary_currency_symbol' => $this->primary->symbol,
'primary_currency_decimal_places' => $this->primary->decimal_places,

View File

@@ -66,16 +66,17 @@ class AvailableBudgetTransformer extends AbstractTransformer
// currencies according to 6.3.0
'object_has_currency_setting' => true,
'currency_id' => (string) $currency->id,
'currency_name' => $currency->name,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
'primary_currency_id' => (string) $this->primary->id,
'primary_currency_name' => $this->primary->name,
'primary_currency_code' => $this->primary->code,
'primary_currency_symbol' => $this->primary->symbol,
'primary_currency_decimal_places' => $this->primary->decimal_places,
'amount' => $amount,
'pc_amount' => $pcAmount,
'start' => $availableBudget->start_date->toAtomString(),

View File

@@ -60,11 +60,13 @@ class BillTransformer extends AbstractTransformer
// currencies according to 6.3.0
'object_has_currency_setting' => true,
'currency_id' => (string) $bill->transaction_currency_id,
'currency_name' => $currency->name,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
'primary_currency_id' => (string) $this->primary->id,
'primary_currency_name' => $this->primary->name,
'primary_currency_code' => $this->primary->code,
'primary_currency_symbol' => $this->primary->symbol,
'primary_currency_decimal_places' => $this->primary->decimal_places,

View File

@@ -27,11 +27,8 @@ namespace FireflyIII\Transformers;
use FireflyIII\Enums\AutoBudgetType;
use FireflyIII\Models\Budget;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Repositories\Budget\OperationsRepositoryInterface;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\Steam;
use Illuminate\Support\Collection;
use Symfony\Component\HttpFoundation\ParameterBag;
/**
@@ -40,20 +37,22 @@ use Symfony\Component\HttpFoundation\ParameterBag;
class BudgetTransformer extends AbstractTransformer
{
private readonly bool $convertToPrimary;
private readonly TransactionCurrency $primary;
private readonly OperationsRepositoryInterface $opsRepository;
private readonly BudgetRepositoryInterface $repository;
private readonly TransactionCurrency $primaryCurrency;
private array $types;
/**
* BudgetTransformer constructor.
*/
public function __construct()
{
$this->opsRepository = app(OperationsRepositoryInterface::class);
$this->repository = app(BudgetRepositoryInterface::class);
$this->parameters = new ParameterBag();
$this->primary = Amount::getPrimaryCurrency();
$this->primaryCurrency = Amount::getPrimaryCurrency();
$this->convertToPrimary = Amount::convertToPrimary();
$this->types = [
AutoBudgetType::AUTO_BUDGET_RESET->value => 'reset',
AutoBudgetType::AUTO_BUDGET_ROLLOVER->value => 'rollover',
AutoBudgetType::AUTO_BUDGET_ADJUSTED->value => 'adjusted',
];
}
/**
@@ -61,73 +60,55 @@ class BudgetTransformer extends AbstractTransformer
*/
public function transform(Budget $budget): array
{
$this->opsRepository->setUser($budget->user);
$start = $this->parameters->get('start');
$end = $this->parameters->get('end');
$autoBudget = $this->repository->getAutoBudget($budget);
$spent = [];
if (null !== $start && null !== $end) {
$spent = $this->beautify($this->opsRepository->sumExpenses($start, $end, null, new Collection([$budget])));
}
// info for auto budget.
$abType = null;
$abAmount = null;
$abPrimary = null;
$abPeriod = null;
$notes = $this->repository->getNoteText($budget);
$types = [
AutoBudgetType::AUTO_BUDGET_RESET->value => 'reset',
AutoBudgetType::AUTO_BUDGET_ROLLOVER->value => 'rollover',
AutoBudgetType::AUTO_BUDGET_ADJUSTED->value => 'adjusted',
];
$currency = $autoBudget?->transactionCurrency;
$primary = $this->primary;
if (!$this->convertToPrimary) {
$primary = null;
}
if (null === $autoBudget) {
$currency = $primary;
}
if (null !== $autoBudget) {
$abType = $types[$autoBudget->auto_budget_type];
$abAmount = Steam::bcround($autoBudget->amount, $currency->decimal_places);
$abPrimary = $this->convertToPrimary ? Steam::bcround($autoBudget->native_amount, $primary->decimal_places) : null;
$abPeriod = $autoBudget->period;
$currency = $budget->meta['currency'] ?? null;
if (null !== $budget->meta['auto_budget']) {
$abType = $this->types[$budget->meta['auto_budget']['type']];
$abAmount = Steam::bcround($budget->meta['auto_budget']['amount'], $currency->decimal_places);
$abPrimary = $this->convertToPrimary ? Steam::bcround($budget->meta['auto_budget']['pc_amount'], $this->primaryCurrency->decimal_places) : null;
$abPeriod = $budget->meta['auto_budget']['period'];
}
return [
'id' => (string) $budget->id,
'id' => (string)$budget->id,
'created_at' => $budget->created_at->toAtomString(),
'updated_at' => $budget->updated_at->toAtomString(),
'active' => $budget->active,
'name' => $budget->name,
'order' => $budget->order,
'notes' => $notes,
'notes' => $budget->meta['notes'],
'auto_budget_type' => $abType,
'auto_budget_period' => $abPeriod,
'currency_id' => null === $autoBudget ? null : (string) $autoBudget->transactionCurrency->id,
'currency_code' => $autoBudget?->transactionCurrency->code,
'currency_name' => $autoBudget?->transactionCurrency->name,
'currency_decimal_places' => $autoBudget?->transactionCurrency->decimal_places,
'currency_symbol' => $autoBudget?->transactionCurrency->symbol,
// new currency settings.
'object_has_currency_setting' => null !== $budget->meta['currency'],
'currency_id' => null === $currency ? null : (string)$currency->id,
'currency_code' => $currency?->code,
'currency_name' => $currency?->name,
'currency_symbol' => $currency?->symbol,
'currency_decimal_places' => $currency?->decimal_places,
'primary_currency_id' => $primary instanceof TransactionCurrency ? (string) $primary->id : null,
'primary_currency_code' => $primary?->code,
'primary_currency_symbol' => $primary?->symbol,
'primary_currency_decimal_places' => $primary?->decimal_places,
// amount and primary currency amount if present.
'primary_currency_id' => (string)$this->primaryCurrency->id,
'primary_currency_name' => $this->primaryCurrency->name,
'primary_currency_code' => $this->primaryCurrency->code,
'primary_currency_symbol' => $this->primaryCurrency->symbol,
'primary_currency_decimal_places' => $this->primaryCurrency->decimal_places,
'auto_budget_amount' => $abAmount,
'pc_auto_budget_amount' => $abPrimary,
'spent' => $spent, // always in primary currency.
'spent' => $this->beautify($budget->meta['spent']), // always in primary currency.
'pc_spent' => $this->beautify($budget->meta['pc_spent']), // always in primary currency.
'links' => [
[
'rel' => 'self',
'uri' => '/budgets/'.$budget->id,
'uri' => '/budgets/' . $budget->id,
],
],
];
@@ -137,7 +118,7 @@ class BudgetTransformer extends AbstractTransformer
{
$return = [];
foreach ($array as $data) {
$data['sum'] = Steam::bcround($data['sum'], (int) $data['currency_decimal_places']);
$data['sum'] = Steam::bcround($data['sum'], (int)$data['currency_decimal_places']);
$return[] = $data;
}

View File

@@ -34,6 +34,7 @@
{{ CurrencyForm.currencyList('auto_budget_currency_id', autoBudget.transaction_currency_id) }}
{{ ExpandedForm.amountNoCurrency('auto_budget_amount', preFilled.auto_budget_amount) }}
{{ ExpandedForm.select('auto_budget_period', autoBudgetPeriods, autoBudget.period) }}
{{ ExpandedForm.textarea('notes',preFilled.notes,{helpText: trans('firefly.field_supports_markdown')}) }}
{{ ExpandedForm.file('attachments[]', {'multiple': 'multiple','helpText': trans('firefly.upload_max_file_size', {'size': uploadSize|filesize}) }) }}
</div>
</div>