diff --git a/app/Api/V2/Controllers/Model/Bill/ShowController.php b/app/Api/V2/Controllers/Model/Bill/ShowController.php index b9ad71529c..a0c1cdbfff 100644 --- a/app/Api/V2/Controllers/Model/Bill/ShowController.php +++ b/app/Api/V2/Controllers/Model/Bill/ShowController.php @@ -1,4 +1,6 @@ . + */ + +namespace FireflyIII\Api\V2\Controllers\Model\PiggyBank; + +use FireflyIII\Api\V2\Controllers\Controller; +use FireflyIII\Repositories\Administration\PiggyBank\PiggyBankRepositoryInterface; +use FireflyIII\Transformers\V2\PiggyBankTransformer; +use Illuminate\Http\JsonResponse; +use Illuminate\Http\Request; +use Illuminate\Pagination\LengthAwarePaginator; + +/** + * Class ShowController + */ +class ShowController extends Controller +{ + private PiggyBankRepositoryInterface $repository; + + public function __construct() + { + parent::__construct(); + $this->middleware( + function ($request, $next) { + $this->repository = app(PiggyBankRepositoryInterface::class); + $this->repository->setAdministrationId(auth()->user()->user_group_id); + return $next($request); + } + ); + } + + /** + * @param Request $request + * + * TODO see autocomplete/accountcontroller for list. + * + * @return JsonResponse + */ + public function index(Request $request): JsonResponse + { + $piggies = $this->repository->getPiggyBanks(); + $pageSize = (int)app('preferences')->getForUser(auth()->user(), 'listPageSize', 50)->data; + $count = $piggies->count(); + $piggies = $piggies->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize); + $paginator = new LengthAwarePaginator($piggies, $count, $pageSize, $this->parameters->get('page')); + $transformer = new PiggyBankTransformer(); + $transformer->setParameters($this->parameters); // give params to transformer + + return response() + ->json($this->jsonApiList('piggy-banks', $paginator, $transformer)) + ->header('Content-Type', self::CONTENT_TYPE); + } +} diff --git a/app/Api/V2/Controllers/Transaction/List/AccountController.php b/app/Api/V2/Controllers/Transaction/List/AccountController.php index d00e08a81e..d0ce8d858f 100644 --- a/app/Api/V2/Controllers/Transaction/List/AccountController.php +++ b/app/Api/V2/Controllers/Transaction/List/AccountController.php @@ -80,9 +80,11 @@ class AccountController extends Controller $paginator = $collector->getPaginatedGroups(); $paginator->setPath( - sprintf('%s?%s', - route('api.v2.accounts.transactions', [$account->id]), - $request->buildParams()) + sprintf( + '%s?%s', + route('api.v2.accounts.transactions', [$account->id]), + $request->buildParams() + ) ); return response() diff --git a/app/Api/V2/Controllers/Transaction/List/TransactionController.php b/app/Api/V2/Controllers/Transaction/List/TransactionController.php index a68e344498..48d5967746 100644 --- a/app/Api/V2/Controllers/Transaction/List/TransactionController.php +++ b/app/Api/V2/Controllers/Transaction/List/TransactionController.php @@ -1,4 +1,6 @@ getPaginatedGroups(); $paginator->setPath( - sprintf('%s?%s', - route('api.v2.transactions.list'), - $request->buildParams()) + sprintf( + '%s?%s', + route('api.v2.transactions.list'), + $request->buildParams() + ) ); return response() diff --git a/app/Models/UserGroup.php b/app/Models/UserGroup.php index af28168786..8f2bd8581e 100644 --- a/app/Models/UserGroup.php +++ b/app/Models/UserGroup.php @@ -29,6 +29,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Support\Carbon; /** @@ -106,6 +107,16 @@ class UserGroup extends Model return $this->hasMany(GroupMembership::class); } + /** + * Link to piggy banks. + * + * @return HasManyThrough + */ + public function piggyBanks(): HasManyThrough + { + return $this->hasManyThrough(PiggyBank::class, Account::class); + } + /** * Link to transaction journals. * diff --git a/app/Providers/PiggyBankServiceProvider.php b/app/Providers/PiggyBankServiceProvider.php index 05e0514419..c05aa49467 100644 --- a/app/Providers/PiggyBankServiceProvider.php +++ b/app/Providers/PiggyBankServiceProvider.php @@ -25,6 +25,10 @@ namespace FireflyIII\Providers; use FireflyIII\Repositories\PiggyBank\PiggyBankRepository; use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface; + +use FireflyIII\Repositories\Administration\PiggyBank\PiggyBankRepository as AdminPiggyBankRepository; +use FireflyIII\Repositories\Administration\PiggyBank\PiggyBankRepositoryInterface as AdminPiggyBankRepositoryInterface; + use Illuminate\Foundation\Application; use Illuminate\Support\ServiceProvider; @@ -36,9 +40,7 @@ class PiggyBankServiceProvider extends ServiceProvider /** * Bootstrap the application services. */ - public function boot(): void - { - } + public function boot(): void {} /** * Register the application services. @@ -57,5 +59,17 @@ class PiggyBankServiceProvider extends ServiceProvider return $repository; } ); + + $this->app->bind( + AdminPiggyBankRepositoryInterface::class, + function (Application $app) { + /** @var AdminPiggyBankRepository $repository */ + $repository = app(AdminPiggyBankRepository::class); + if ($app->auth->check()) { // @phpstan-ignore-line (phpstan does not understand the reference to auth) + $repository->setUser(auth()->user()); + } + return $repository; + } + ); } } diff --git a/app/Repositories/Administration/PiggyBank/PiggyBankRepository.php b/app/Repositories/Administration/PiggyBank/PiggyBankRepository.php new file mode 100644 index 0000000000..49b3c5413e --- /dev/null +++ b/app/Repositories/Administration/PiggyBank/PiggyBankRepository.php @@ -0,0 +1,50 @@ +. + */ + +namespace FireflyIII\Repositories\Administration\PiggyBank; + +use FireflyIII\Support\Repositories\Administration\AdministrationTrait; +use Illuminate\Support\Collection; + +/** + * Class PiggyBankRepository + */ +class PiggyBankRepository implements PiggyBankRepositoryInterface +{ + use AdministrationTrait; + + /** + * @inheritDoc + */ + public function getPiggyBanks(): Collection + { + return $this->userGroup->piggyBanks() + ->with( + [ + 'account', + 'objectGroups', + ] + ) + ->orderBy('order', 'ASC')->get(); + } +} diff --git a/app/Repositories/Administration/PiggyBank/PiggyBankRepositoryInterface.php b/app/Repositories/Administration/PiggyBank/PiggyBankRepositoryInterface.php new file mode 100644 index 0000000000..a128e3827a --- /dev/null +++ b/app/Repositories/Administration/PiggyBank/PiggyBankRepositoryInterface.php @@ -0,0 +1,39 @@ +. + */ + +namespace FireflyIII\Repositories\Administration\PiggyBank; + +use Illuminate\Support\Collection; + +/** + * Interface PiggyBankRepositoryInterface + */ +interface PiggyBankRepositoryInterface +{ + /** + * Return all piggy banks. + * + * @return Collection + */ + public function getPiggyBanks(): Collection; +} diff --git a/app/Transformers/V2/AccountTransformer.php b/app/Transformers/V2/AccountTransformer.php index a7e82727f0..b5afd3e84b 100644 --- a/app/Transformers/V2/AccountTransformer.php +++ b/app/Transformers/V2/AccountTransformer.php @@ -37,10 +37,11 @@ use Illuminate\Support\Collection; */ class AccountTransformer extends AbstractTransformer { - private array $accountMeta; - private array $balances; - private array $currencies; - private ?TransactionCurrency $currency; + private array $accountMeta; + private array $balances; + private array $convertedBalances; + private array $currencies; + private TransactionCurrency $default; /** * @inheritDoc @@ -48,12 +49,12 @@ class AccountTransformer extends AbstractTransformer */ public function collectMetaData(Collection $objects): void { - $this->currency = null; - $this->currencies = []; - $this->accountMeta = []; - $this->balances = app('steam')->balancesByAccounts($objects, $this->getDate()); - $repository = app(CurrencyRepositoryInterface::class); - $this->currency = app('amount')->getDefaultCurrency(); + $this->currencies = []; + $this->accountMeta = []; + $this->balances = app('steam')->balancesByAccounts($objects, $this->getDate()); + $this->convertedBalances = app('steam')->balancesByAccountsConverted($objects, $this->getDate()); + $repository = app(CurrencyRepositoryInterface::class); + $this->default = app('amount')->getDefaultCurrency(); // get currencies: $accountIds = $objects->pluck('id')->toArray(); @@ -100,10 +101,13 @@ class AccountTransformer extends AbstractTransformer $id = (int)$account->id; // no currency? use default - $currency = $this->currency; + $currency = $this->default; if (0 !== (int)$this->accountMeta[$id]['currency_id']) { $currency = $this->currencies[(int)$this->accountMeta[$id]['currency_id']]; } + // amounts and calculation. + $balance = $this->balances[$id] ?? null; + $nativeBalance = $this->convertedBalances[$id]['native_balance'] ?? null; return [ 'id' => (string)$account->id, @@ -112,19 +116,30 @@ class AccountTransformer extends AbstractTransformer 'active' => $account->active, //'order' => $order, 'name' => $account->name, + 'iban' => '' === $account->iban ? null : $account->iban, // 'type' => strtolower($accountType), // 'account_role' => $accountRole, - 'currency_id' => $currency->id, + 'currency_id' => (string)$currency->id, 'currency_code' => $currency->code, 'currency_symbol' => $currency->symbol, - 'currency_decimal_places' => $currency->decimal_places, - 'current_balance' => $this->balances[$id] ?? null, - 'current_balance_date' => $this->getDate(), + 'currency_decimal_places' => (int)$currency->decimal_places, + + 'native_id' => (string)$this->default->id, + 'native_code' => $this->default->code, + 'native_symbol' => $this->default->symbol, + 'native_decimal_places' => (int)$this->default->decimal_places, + + // balance: + 'current_balance' => $balance, + 'native_current_balance' => $nativeBalance, + 'current_balance_date' => $this->getDate(), + + // more meta + // 'notes' => $this->repository->getNoteText($account), // 'monthly_payment_date' => $monthlyPaymentDate, // 'credit_card_type' => $creditCardType, // 'account_number' => $this->repository->getMetaValue($account, 'account_number'), - 'iban' => '' === $account->iban ? null : $account->iban, // 'bic' => $this->repository->getMetaValue($account, 'BIC'), // 'virtual_balance' => number_format((float) $account->virtual_balance, $decimalPlaces, '.', ''), // 'opening_balance' => $openingBalance, @@ -138,7 +153,7 @@ class AccountTransformer extends AbstractTransformer // 'longitude' => $longitude, // 'latitude' => $latitude, // 'zoom_level' => $zoomLevel, - 'links' => [ + 'links' => [ [ 'rel' => 'self', 'uri' => '/accounts/' . $account->id, diff --git a/app/Transformers/V2/PiggyBankTransformer.php b/app/Transformers/V2/PiggyBankTransformer.php new file mode 100644 index 0000000000..5db4160413 --- /dev/null +++ b/app/Transformers/V2/PiggyBankTransformer.php @@ -0,0 +1,269 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Transformers\V2; + +use Carbon\Carbon; +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Models\Account; +use FireflyIII\Models\AccountMeta; +use FireflyIII\Models\Note; +use FireflyIII\Models\ObjectGroup; +use FireflyIII\Models\PiggyBank; +use FireflyIII\Models\PiggyBankRepetition; +use FireflyIII\Models\TransactionCurrency; +use FireflyIII\Models\TransactionJournal; +use FireflyIII\Support\Http\Api\ExchangeRateConverter; +use Illuminate\Support\Collection; +use Illuminate\Support\Facades\DB; +use JsonException; + +/** + * Class PiggyBankTransformer + */ +class PiggyBankTransformer extends AbstractTransformer +{ +// private AccountRepositoryInterface $accountRepos; +// private CurrencyRepositoryInterface $currencyRepos; +// private PiggyBankRepositoryInterface $piggyRepos; + private array $accounts; + private ExchangeRateConverter $converter; + private array $currencies; + private TransactionCurrency $default; + private array $groups; + private array $notes; + private array $repetitions; + + /** + * PiggyBankTransformer constructor. + * + + */ + public function __construct() + { + $this->notes = []; + $this->accounts = []; + $this->groups = []; + $this->currencies = []; + $this->repetitions = []; +// $this-> +// $this->currencyRepos = app(CurrencyRepositoryInterface::class); +// $this->piggyRepos = app(PiggyBankRepositoryInterface::class); + } + + /** + * @inheritDoc + */ + public function collectMetaData(Collection $objects): void + { + // TODO move to repository (does not exist yet) + $piggyBanks = $objects->pluck('id')->toArray(); + $accountInfo = Account::whereIn('id', $objects->pluck('account_id')->toArray())->get(); + $currencyPreferences = AccountMeta::where('name', '"currency_id"')->whereIn('account_id', $objects->pluck('account_id')->toArray())->get(); + /** @var Account $account */ + foreach ($accountInfo as $account) { + $id = (int)$account->id; + $this->accounts[$id] = [ + 'name' => $account->name, + ]; + } + /** @var AccountMeta $preference */ + foreach ($currencyPreferences as $preference) { + $currencyId = (int)$preference->data; + $accountId = (int)$preference->account_id; + $currencies[$currencyId] = $currencies[$currencyId] ?? TransactionJournal::find($currencyId); + $this->currencies[$accountId] = $currencies[$currencyId]; + } + + // grab object groups: + $set = DB::table('object_groupables') + ->leftJoin('object_groups', 'object_groups.id', '=', 'object_groupables.object_group_id') + ->where('object_groupables.object_groupable_type', PiggyBank::class) + ->get(['object_groupables.*', 'object_groups.title', 'object_groups.order']); + /** @var ObjectGroup $entry */ + foreach ($set as $entry) { + $piggyBankId = (int)$entry->object_groupable_id; + $id = (int)$entry->object_group_id; + $order = (int)$entry->order; + $this->groups[$piggyBankId] = [ + 'object_group_id' => $id, + 'object_group_title' => $entry->title, + 'object_group_order' => $order, + ]; + + } + + // grab repetitions (for current amount): + $repetitions = PiggyBankRepetition::whereIn('piggy_bank_id', $piggyBanks)->get(); + /** @var PiggyBankRepetition $repetition */ + foreach ($repetitions as $repetition) { + $this->repetitions[(int)$repetition->piggy_bank_id] = [ + 'amount' => $repetition->currentamount, + ]; + } + + // grab notes + // continue with notes + $notes = Note::whereNoteableType(PiggyBank::class)->whereIn('noteable_id', array_keys($piggyBanks))->get(); + /** @var Note $note */ + foreach ($notes as $note) { + $id = (int)$note->noteable_id; + $this->notes[$id] = $note; + } + + $this->default = app('amount')->getDefaultCurrencyByUser(auth()->user()); + $this->converter = new ExchangeRateConverter(); + } + + /** + * Transform the piggy bank. + * + * @param PiggyBank $piggyBank + * + * @return array + * @throws FireflyException + * @throws JsonException + */ + public function transform(PiggyBank $piggyBank): array + { +// $account = $piggyBank->account; +// $this->accountRepos->setUser($account->user); +// $this->currencyRepos->setUser($account->user); +// $this->piggyRepos->setUser($account->user); + + // get currency from account, or use default. +// $currency = $this->accountRepos->getAccountCurrency($account) ?? app('amount')->getDefaultCurrencyByUser($account->user); + + // note +// $notes = $this->piggyRepos->getNoteText($piggyBank); +// $notes = '' === $notes ? null : $notes; + +// $objectGroupId = null; +// $objectGroupOrder = null; +// $objectGroupTitle = null; +// /** @var ObjectGroup $objectGroup */ +// $objectGroup = $piggyBank->objectGroups->first(); +// if (null !== $objectGroup) { +// $objectGroupId = (int)$objectGroup->id; +// $objectGroupOrder = (int)$objectGroup->order; +// $objectGroupTitle = $objectGroup->title; +// } + + // get currently saved amount: +// $currentAmount = app('steam')->bcround($this->piggyRepos->getCurrentAmount($piggyBank), $currency->decimal_places); + + $percentage = null; + $leftToSave = null; + $nativeLeftToSave = null; + $savePerMonth = null; + $nativeSavePerMonth = null; + $startDate = $piggyBank->startdate?->format('Y-m-d'); + $targetDate = $piggyBank->targetdate?->format('Y-m-d'); + $accountId = (int)$piggyBank->account_id; + $accountName = $this->accounts[$accountId]['name'] ?? null; + $currency = $this->currencies[$accountId] ?? $this->default; + $currentAmount = app('steam')->bcround($this->repetitions[(int)$piggyBank->id]['amount'] ?? '0', $currency->decimal_places); + $nativeCurrentAmount = $this->converter->convert($this->default, $currency, today(), $currentAmount); + $targetAmount = $piggyBank->targetamount; + $nativeTargetAmount = $this->converter->convert($this->default, $currency, today(), $targetAmount); + $note = $this->notes[(int)$piggyBank->id] ?? null; + $group = $this->groups[(int)$piggyBank->id] ?? null; + + if (0 !== bccomp($targetAmount, '0')) { // target amount is not 0.00 + $leftToSave = bcsub($targetAmount, $currentAmount); + $nativeLeftToSave = $this->converter->convert($this->default, $currency, today(), $leftToSave); + $percentage = (int)bcmul(bcdiv($currentAmount, $targetAmount), '100'); + $savePerMonth = $this->getSuggestedMonthlyAmount($currentAmount, $targetAmount, $piggyBank->startdate, $piggyBank->targetdate); + $nativeSavePerMonth = $this->converter->convert($this->default, $currency, today(), $savePerMonth); + } + + return [ + 'id' => (string)$piggyBank->id, + 'created_at' => $piggyBank->created_at->toAtomString(), + 'updated_at' => $piggyBank->updated_at->toAtomString(), + 'account_id' => (string)$piggyBank->account_id, + 'account_name' => $accountName, + 'name' => $piggyBank->name, + 'currency_id' => (string)$currency->id, + 'currency_code' => $currency->code, + 'currency_symbol' => $currency->symbol, + 'currency_decimal_places' => (int)$currency->decimal_places, + 'native_id' => (string)$this->default->id, + 'native_code' => $this->default->code, + 'native_symbol' => $this->default->symbol, + 'native_decimal_places' => (int)$this->default->decimal_places, + 'current_amount' => $currentAmount, + 'native_current_amount' => $nativeCurrentAmount, + 'target_amount' => $targetAmount, + 'native_target_amount' => $nativeTargetAmount, + 'percentage' => $percentage, + 'left_to_save' => $leftToSave, + 'native_left_to_save' => $nativeLeftToSave, + 'save_per_month' => $savePerMonth, + 'native_save_per_month' => $nativeSavePerMonth, + 'start_date' => $startDate, + 'target_date' => $targetDate, + 'order' => (int)$piggyBank->order, + 'active' => $piggyBank->active, + 'notes' => $note, + 'object_group_id' => $group ? $group['object_group_id'] : null, + 'object_group_order' => $group ? $group['object_group_order'] : null, + 'object_group_title' => $group ? $group['object_group_title'] : null, + 'links' => [ + [ + 'rel' => 'self', + 'uri' => '/piggy_banks/' . $piggyBank->id, + ], + ], + ]; + } + + /** + * @return string|null + */ + private function getSuggestedMonthlyAmount(string $currentAmount, string $targetAmount, ?Carbon $startDate, ?Carbon $targetDate): string + { + $savePerMonth = '0'; + if (null === $targetDate) { + return '0'; + } + if (bccomp($currentAmount, $targetAmount) < 1) { + $now = today(config('app.timezone')); + $startDate = null !== $startDate && $startDate->gte($now) ? $startDate : $now; + $diffInMonths = $startDate->diffInMonths($targetDate, false); + $remainingAmount = bcsub($targetAmount, $currentAmount); + + // more than 1 month to go and still need money to save: + if ($diffInMonths > 0 && 1 === bccomp($remainingAmount, '0')) { + $savePerMonth = bcdiv($remainingAmount, (string)$diffInMonths); + } + + // less than 1 month to go but still need money to save: + if (0 === $diffInMonths && 1 === bccomp($remainingAmount, '0')) { + $savePerMonth = $remainingAmount; + } + } + + return $savePerMonth; + } +} diff --git a/routes/api.php b/routes/api.php index 46a67cb10e..c77a37d9af 100644 --- a/routes/api.php +++ b/routes/api.php @@ -138,6 +138,21 @@ Route::group( Route::get('sum/unpaid', ['uses' => 'SumController@unpaid', 'as' => 'sum.unpaid']); } ); + +/** + * V2 API route for piggy banks. + */ +Route::group( + [ + 'namespace' => 'FireflyIII\Api\V2\Controllers\Model\PiggyBank', + 'prefix' => 'v2/piggy-banks', + 'as' => 'api.v2.piggy-banks.', + ], + static function () { + Route::get('', ['uses' => 'ShowController@index', 'as' => 'index']); + } +); + /** * V2 API route for budgets and budget limits: */