Enrich categories.

This commit is contained in:
James Cole
2025-08-04 20:55:31 +02:00
parent 902d91ad29
commit a4021ff056
7 changed files with 256 additions and 41 deletions

View File

@@ -28,7 +28,9 @@ use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Category;
use FireflyIII\Repositories\Category\CategoryRepositoryInterface;
use FireflyIII\Support\JsonApi\Enrichments\CategoryEnrichment;
use FireflyIII\Transformers\CategoryTransformer;
use FireflyIII\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Pagination\LengthAwarePaginator;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
@@ -78,6 +80,15 @@ class ShowController extends Controller
$count = $collection->count();
$categories = $collection->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize);
// enrich
/** @var User $admin */
$admin = auth()->user();
$enrichment = new CategoryEnrichment();
$enrichment->setUser($admin);
$enrichment->setStart($this->parameters->get('start'));
$enrichment->setEnd($this->parameters->get('end'));
$categories = $enrichment->enrich($categories);
// make paginator:
$paginator = new LengthAwarePaginator($categories, $count, $pageSize, $this->parameters->get('page'));
$paginator->setPath(route('api.v1.categories.index').$this->buildParams());
@@ -105,6 +116,15 @@ class ShowController extends Controller
$transformer = app(CategoryTransformer::class);
$transformer->setParameters($this->parameters);
// enrich
/** @var User $admin */
$admin = auth()->user();
$enrichment = new CategoryEnrichment();
$enrichment->setUser($admin);
$enrichment->setStart($this->parameters->get('start'));
$enrichment->setEnd($this->parameters->get('end'));
$category = $enrichment->enrichSingle($category);
$resource = new Item($category, $transformer, 'categories');
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);

View File

@@ -27,6 +27,7 @@ namespace FireflyIII\Repositories\Category;
use Carbon\Carbon;
use FireflyIII\Enums\TransactionTypeEnum;
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Models\Category;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Report\Summarizer\TransactionSummarizer;
use FireflyIII\Support\Repositories\UserGroup\UserGroupInterface;
@@ -444,4 +445,71 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn
return $array;
}
public function collectExpenses(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $categories = null): array
{
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setUser($this->user)->setRange($start, $end)->setTypes([TransactionTypeEnum::WITHDRAWAL->value]);
if ($accounts instanceof Collection && $accounts->count() > 0) {
$collector->setAccounts($accounts);
}
if (!$categories instanceof Collection || 0 === $categories->count()) {
$categories = $this->getCategories();
}
$collector->setCategories($categories);
$collector->withCategoryInformation();
return $collector->getExtractedJournals();
}
public function collectIncome(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $categories = null): array
{
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setUser($this->user)->setRange($start, $end)
->setTypes([TransactionTypeEnum::DEPOSIT->value])
;
if ($accounts instanceof Collection && $accounts->count() > 0) {
$collector->setAccounts($accounts);
}
if (!$categories instanceof Collection || 0 === $categories->count()) {
$categories = $this->getCategories();
}
$collector->setCategories($categories);
return $collector->getExtractedJournals();
}
public function collectTransfers(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $categories = null): array
{
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setUser($this->user)->setRange($start, $end)
->setTypes([TransactionTypeEnum::TRANSFER->value])
;
if ($accounts instanceof Collection && $accounts->count() > 0) {
$collector->setAccounts($accounts);
}
if (!$categories instanceof Collection || 0 === $categories->count()) {
$categories = $this->getCategories();
}
$collector->setCategories($categories);
return $collector->getExtractedJournals();
}
public function sumCollectedTransactionsByCategory(array $expenses, Category $category, string $method, 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 ($category): bool {
return $expense['category_id'] === $category->id;
});
return $summarizer->groupByCurrencyId($expenses, $method, false);
}
}

View File

@@ -26,6 +26,8 @@ namespace FireflyIII\Repositories\Category;
use Carbon\Carbon;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Models\Category;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Models\UserGroup;
use FireflyIII\User;
use Illuminate\Contracts\Auth\Authenticatable;
@@ -78,6 +80,11 @@ interface OperationsRepositoryInterface
*/
public function sumExpenses(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $categories = null): array;
public function collectExpenses(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $categories = null): array;
public function collectIncome(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $categories = null): array;
public function collectTransfers(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $categories = null): array;
public function sumCollectedTransactionsByCategory(array $expenses, Category $category, string $method, bool $convertToPrimary = false): array;
/**
* Sum of income journals in period for a set of categories, grouped per currency. Amounts are always positive.
*/

View File

@@ -77,6 +77,7 @@ class BudgetEnrichment implements EnrichmentInterface
foreach ($this->collection as $budget) {
$this->ids[] = (int)$budget->id;
}
$this->ids = array_unique($this->ids);
}
private function collectNotes(): void

View File

@@ -0,0 +1,134 @@
<?php
namespace FireflyIII\Support\JsonApi\Enrichments;
use Carbon\Carbon;
use FireflyIII\Models\Category;
use FireflyIII\Models\Note;
use FireflyIII\Models\UserGroup;
use FireflyIII\Repositories\Category\OperationsRepositoryInterface;
use FireflyIII\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
class CategoryEnrichment implements EnrichmentInterface
{
private Collection $collection;
private User $user;
private UserGroup $userGroup;
private array $ids = [];
private array $notes = [];
private ?Carbon $start = null;
private ?Carbon $end = null;
private array $spent = [];
private array $pcSpent = [];
private array $earned = [];
private array $pcEarned = [];
private array $transfers = [];
private array $pcTransfers = [];
public function enrich(Collection $collection): Collection
{
$this->collection = $collection;
$this->collectIds();
$this->collectNotes();
$this->collectTransactions();
$this->appendCollectedData();
return $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 Category $category */
foreach ($this->collection as $category) {
$this->ids[] = (int)$category->id;
}
$this->ids = array_unique($this->ids);
}
private function appendCollectedData(): void
{
$this->collection = $this->collection->map(function (Category $item) {
$id = (int)$item->id;
$meta = [
'notes' => $this->notes[$id] ?? null,
'spent' => $this->spent[$id] ?? null,
'pc_spent' => $this->pcSpent[$id] ?? null,
'earned' => $this->earned[$id] ?? null,
'pc_earned' => $this->pcEarned[$id] ?? null,
'transfers' => $this->transfers[$id] ?? null,
'pc_transfers' => $this->pcTransfers[$id] ?? null,
];
$item->meta = $meta;
return $item;
});
}
public function setEnd(?Carbon $end): void
{
$this->end = $end;
}
public function setStart(?Carbon $start): void
{
$this->start = $start;
}
private function collectNotes(): void
{
$notes = Note::query()->whereIn('noteable_id', $this->ids)
->whereNotNull('notes.text')
->where('notes.text', '!=', '')
->where('noteable_type', Category::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 collectTransactions(): void
{
if (null !== $this->start && null !== $this->end) {
/** @var OperationsRepositoryInterface $opsRepository */
$opsRepository = app(OperationsRepositoryInterface::class);
$opsRepository->setUser($this->user);
$opsRepository->setUserGroup($this->userGroup);
$expenses = $opsRepository->collectExpenses($this->start, $this->end, null, $this->collection);
$income = $opsRepository->collectIncome($this->start, $this->end, null, $this->collection);
$transfers = $opsRepository->collectTransfers($this->start, $this->end, null, $this->collection);
foreach ($this->collection as $item) {
$id = (int)$item->id;
$this->spent[$id] = array_values($opsRepository->sumCollectedTransactionsByCategory($expenses, $item, 'negative', false));
$this->pcSpent[$id] = array_values($opsRepository->sumCollectedTransactionsByCategory($expenses, $item, 'negative', true));
$this->earned[$id] = array_values($opsRepository->sumCollectedTransactionsByCategory($income, $item, 'positive', false));
$this->pcEarned[$id] = array_values($opsRepository->sumCollectedTransactionsByCategory($income, $item, 'positive', true));
$this->transfers[$id] = array_values($opsRepository->sumCollectedTransactionsByCategory($transfers, $item, 'positive', false));
$this->pcTransfers[$id] = array_values($opsRepository->sumCollectedTransactionsByCategory($transfers, $item, 'positive', true));
}
}
}
}

View File

@@ -103,8 +103,8 @@ class BudgetTransformer extends AbstractTransformer
'auto_budget_amount' => $abAmount,
'pc_auto_budget_amount' => $abPrimary,
'spent' => $this->beautify($budget->meta['spent']), // always in primary currency.
'pc_spent' => $this->beautify($budget->meta['pc_spent']), // always in primary currency.
'spent' => $this->beautify($budget->meta['spent']),
'pc_spent' => $this->beautify($budget->meta['pc_spent']),
'links' => [
[
'rel' => 'self',

View File

@@ -26,30 +26,22 @@ namespace FireflyIII\Transformers;
use FireflyIII\Models\Category;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\Category\CategoryRepositoryInterface;
use FireflyIII\Repositories\Category\OperationsRepositoryInterface;
use FireflyIII\Support\Facades\Amount;
use Illuminate\Support\Collection;
use FireflyIII\Support\Facades\Steam;
/**
* Class CategoryTransformer
*/
class CategoryTransformer extends AbstractTransformer
{
private readonly bool $convertToNative;
private readonly TransactionCurrency $primary;
private readonly OperationsRepositoryInterface $opsRepository;
private readonly CategoryRepositoryInterface $repository;
private readonly TransactionCurrency $primaryCurrency;
/**
* CategoryTransformer constructor.
*/
public function __construct()
{
$this->opsRepository = app(OperationsRepositoryInterface::class);
$this->repository = app(CategoryRepositoryInterface::class);
$this->primary = Amount::getPrimaryCurrency();
$this->convertToNative = Amount::convertToPrimary();
$this->primaryCurrency = Amount::getPrimaryCurrency();
}
/**
@@ -57,39 +49,32 @@ class CategoryTransformer extends AbstractTransformer
*/
public function transform(Category $category): array
{
$this->opsRepository->setUser($category->user);
$this->repository->setUser($category->user);
$spent = [];
$earned = [];
$start = $this->parameters->get('start');
$end = $this->parameters->get('end');
if (null !== $start && null !== $end) {
$earned = $this->beautify($this->opsRepository->sumIncome($start, $end, null, new Collection([$category])));
$spent = $this->beautify($this->opsRepository->sumExpenses($start, $end, null, new Collection([$category])));
}
$primary = $this->primary;
if (!$this->convertToNative) {
$primary = null;
}
$notes = $this->repository->getNoteText($category);
return [
'id' => $category->id,
'created_at' => $category->created_at->toAtomString(),
'updated_at' => $category->updated_at->toAtomString(),
'name' => $category->name,
'notes' => $notes,
'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,
'spent' => $spent,
'earned' => $earned,
'id' => $category->id,
'created_at' => $category->created_at->toAtomString(),
'updated_at' => $category->updated_at->toAtomString(),
'name' => $category->name,
'notes' => $category->meta['notes'],
// category never has currency settings.
'object_has_currency_setting' => false,
'primary_currency_id' => (string)$this->primaryCurrency->id,
'primary_currency_code' => $this->primaryCurrency->code,
'primary_currency_symbol' => $this->primaryCurrency->symbol,
'primary_currency_decimal_places' => (int)$this->primaryCurrency->decimal_places,
'spent' => $this->beautify($category->meta['spent']),
'pc_spent' => $this->beautify($category->meta['pc_spent']),
'earned' => $this->beautify($category->meta['earned']),
'pc_earned' => $this->beautify($category->meta['pc_earned']),
'transferred' => $this->beautify($category->meta['transfers']),
'pc_transferred' => $this->beautify($category->meta['pc_transfers']),
'links' => [
[
'rel' => 'self',
'uri' => '/categories/'.$category->id,
'uri' => '/categories/' . $category->id,
],
],
];