diff --git a/app/Api/V1/Controllers/Models/Category/ShowController.php b/app/Api/V1/Controllers/Models/Category/ShowController.php index 6210a18a87..080ea5c79b 100644 --- a/app/Api/V1/Controllers/Models/Category/ShowController.php +++ b/app/Api/V1/Controllers/Models/Category/ShowController.php @@ -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); diff --git a/app/Repositories/Category/OperationsRepository.php b/app/Repositories/Category/OperationsRepository.php index 19d76502a9..1c1b9ebb8d 100644 --- a/app/Repositories/Category/OperationsRepository.php +++ b/app/Repositories/Category/OperationsRepository.php @@ -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); + } } diff --git a/app/Repositories/Category/OperationsRepositoryInterface.php b/app/Repositories/Category/OperationsRepositoryInterface.php index 6ff6d9bf78..689a7ec875 100644 --- a/app/Repositories/Category/OperationsRepositoryInterface.php +++ b/app/Repositories/Category/OperationsRepositoryInterface.php @@ -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. */ diff --git a/app/Support/JsonApi/Enrichments/BudgetEnrichment.php b/app/Support/JsonApi/Enrichments/BudgetEnrichment.php index 2de1eb1156..d25c12fd4e 100644 --- a/app/Support/JsonApi/Enrichments/BudgetEnrichment.php +++ b/app/Support/JsonApi/Enrichments/BudgetEnrichment.php @@ -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 diff --git a/app/Support/JsonApi/Enrichments/CategoryEnrichment.php b/app/Support/JsonApi/Enrichments/CategoryEnrichment.php new file mode 100644 index 0000000000..128bb10b92 --- /dev/null +++ b/app/Support/JsonApi/Enrichments/CategoryEnrichment.php @@ -0,0 +1,134 @@ +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)); + } + } + } + +} diff --git a/app/Transformers/BudgetTransformer.php b/app/Transformers/BudgetTransformer.php index 1c7d085372..35c4d6c6a9 100644 --- a/app/Transformers/BudgetTransformer.php +++ b/app/Transformers/BudgetTransformer.php @@ -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', diff --git a/app/Transformers/CategoryTransformer.php b/app/Transformers/CategoryTransformer.php index 19b26b2d11..d6d93ca636 100644 --- a/app/Transformers/CategoryTransformer.php +++ b/app/Transformers/CategoryTransformer.php @@ -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, ], ], ];