. */ declare(strict_types=1); namespace FireflyIII\Repositories\PiggyBank; use Exception; use FireflyIII\Events\ChangedPiggyBankAmount; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Note; use FireflyIII\Models\PiggyBank; use FireflyIII\Models\PiggyBankRepetition; use FireflyIII\Models\TransactionJournal; use FireflyIII\Repositories\ObjectGroup\CreatesObjectGroups; use Illuminate\Database\QueryException; use Illuminate\Support\Facades\Log; /** * Trait ModifiesPiggyBanks */ trait ModifiesPiggyBanks { use CreatesObjectGroups; /** * @param PiggyBank $piggyBank * @param string $amount * @param TransactionJournal|null $journal * @return bool */ public function addAmount(PiggyBank $piggyBank, string $amount, ?TransactionJournal $journal = null): bool { $repetition = $this->getRepetition($piggyBank); if (null === $repetition) { return false; } $currentAmount = $repetition->currentamount ?? '0'; $repetition->currentamount = bcadd($currentAmount, $amount); $repetition->save(); Log::debug('addAmount: Trigger change for positive amount.'); event(new ChangedPiggyBankAmount($piggyBank, $amount, $journal, null)); return true; } /** * @param PiggyBankRepetition $repetition * @param string $amount * * @return void */ public function addAmountToRepetition(PiggyBankRepetition $repetition, string $amount, TransactionJournal $journal): void { Log::debug(sprintf('addAmountToRepetition: %s', $amount)); if (-1 === bccomp($amount, '0')) { Log::debug('Remove amount.'); $this->removeAmount($repetition->piggyBank, bcmul($amount, '-1'), $journal); } if (1 === bccomp($amount, '0')) { Log::debug('Add amount.'); $this->addAmount($repetition->piggyBank, $amount, $journal); } } /** * @param PiggyBank $piggyBank * @param string $amount * * @return bool */ public function canAddAmount(PiggyBank $piggyBank, string $amount): bool { $today = today(config('app.timezone')); $leftOnAccount = $this->leftOnAccount($piggyBank, $today); $savedSoFar = (string)$this->getRepetition($piggyBank)->currentamount; $maxAmount = $leftOnAccount; $leftToSave = null; if (0 !== bccomp($piggyBank->targetamount, '0')) { $leftToSave = bcsub($piggyBank->targetamount, $savedSoFar); $maxAmount = 1 === bccomp($leftOnAccount, $leftToSave) ? $leftToSave : $leftOnAccount; } $compare = bccomp($amount, $maxAmount); $result = $compare <= 0; Log::debug(sprintf('Left on account: %s on %s', $leftOnAccount, $today->format('Y-m-d'))); Log::debug(sprintf('Saved so far: %s', $savedSoFar)); Log::debug(sprintf('Left to save: %s', $leftToSave)); Log::debug(sprintf('Maximum amount: %s', $maxAmount)); Log::debug(sprintf('Compare <= 0? %d, so %s', $compare, var_export($result, true))); return $result; } /** * @param PiggyBank $piggyBank * @param string $amount * * @return bool */ public function canRemoveAmount(PiggyBank $piggyBank, string $amount): bool { $repetition = $this->getRepetition($piggyBank); if (null === $repetition) { return false; } $savedSoFar = $repetition->currentamount; return bccomp($amount, $savedSoFar) <= 0; } /** * @param PiggyBank $piggyBank * * @return bool * @throws Exception */ public function destroy(PiggyBank $piggyBank): bool { $piggyBank->objectGroups()->sync([]); $piggyBank->delete(); return true; } /** * @param PiggyBank $piggyBank * @param string $amount * * @return bool */ public function removeAmount(PiggyBank $piggyBank, string $amount, ?TransactionJournal $journal = null): bool { $repetition = $this->getRepetition($piggyBank); if (null === $repetition) { return false; } $repetition->currentamount = bcsub($repetition->currentamount, $amount); $repetition->save(); Log::debug('addAmount: Trigger change for negative amount.'); event(new ChangedPiggyBankAmount($piggyBank, bcmul($amount, '-1'), $journal, null)); return true; } /** * @inheritDoc */ public function removeObjectGroup(PiggyBank $piggyBank): PiggyBank { $piggyBank->objectGroups()->sync([]); return $piggyBank; } /** * @param PiggyBank $piggyBank * @param string $amount * * @return PiggyBank */ public function setCurrentAmount(PiggyBank $piggyBank, string $amount): PiggyBank { $repetition = $this->getRepetition($piggyBank); if (null === $repetition) { return $piggyBank; } $max = $piggyBank->targetamount; if (1 === bccomp($amount, $max)) { $amount = $max; } $difference = bcsub($amount, $repetition->currentamount); $repetition->currentamount = $amount; $repetition->save(); if (-1 === bccomp($difference, '0')) { Log::debug('addAmount: Trigger change for negative amount.'); event(new ChangedPiggyBankAmount($piggyBank, bcmul($amount, '-1'), null, null)); } if (1 === bccomp($difference, '0')) { Log::debug('addAmount: Trigger change for positive amount.'); event(new ChangedPiggyBankAmount($piggyBank, $amount, null, null)); } return $piggyBank; } /** * @inheritDoc */ public function setObjectGroup(PiggyBank $piggyBank, string $objectGroupTitle): PiggyBank { $objectGroup = $this->findOrCreateObjectGroup($objectGroupTitle); if (null !== $objectGroup) { $piggyBank->objectGroups()->sync([$objectGroup->id]); } return $piggyBank; } /** * @param array $data * * @return PiggyBank * @throws FireflyException */ public function store(array $data): PiggyBank { $order = $this->getMaxOrder() + 1; if (array_key_exists('order', $data)) { $order = $data['order']; } $data['order'] = 31337; // very high when creating. $piggyData = $data; // unset fields just in case. unset($piggyData['object_group_title'], $piggyData['object_group_id'], $piggyData['notes'], $piggyData['current_amount']); // validate amount: if (array_key_exists('targetamount', $piggyData) && '' === (string)$piggyData['targetamount']) { $piggyData['targetamount'] = '0'; } try { /** @var PiggyBank $piggyBank */ $piggyBank = PiggyBank::create($piggyData); } catch (QueryException $e) { Log::error(sprintf('Could not store piggy bank: %s', $e->getMessage()), $piggyData); throw new FireflyException('400005: Could not store new piggy bank.', 0, $e); } // reset order then set order: $this->resetOrder(); $this->setOrder($piggyBank, $order); $this->updateNote($piggyBank, $data['notes']); // repetition is auto created. $repetition = $this->getRepetition($piggyBank); if (null !== $repetition && array_key_exists('current_amount', $data) && '' !== $data['current_amount']) { $repetition->currentamount = $data['current_amount']; $repetition->save(); } $objectGroupTitle = $data['object_group_title'] ?? ''; if ('' !== $objectGroupTitle) { $objectGroup = $this->findOrCreateObjectGroup($objectGroupTitle); if (null !== $objectGroup) { $piggyBank->objectGroups()->sync([$objectGroup->id]); $piggyBank->save(); } } // try also with ID $objectGroupId = (int)($data['object_group_id'] ?? 0); if (0 !== $objectGroupId) { $objectGroup = $this->findObjectGroupById($objectGroupId); if (null !== $objectGroup) { $piggyBank->objectGroups()->sync([$objectGroup->id]); $piggyBank->save(); } } return $piggyBank; } /** * Correct order of piggies in case of issues. */ public function resetOrder(): void { $set = $this->user->piggyBanks()->orderBy('piggy_banks.order', 'ASC')->get(['piggy_banks.*']); $current = 1; foreach ($set as $piggyBank) { if ((int)$piggyBank->order !== $current) { Log::debug(sprintf('Piggy bank #%d ("%s") was at place %d but should be on %d', $piggyBank->id, $piggyBank->name, $piggyBank->order, $current)); $piggyBank->order = $current; $piggyBank->save(); } $current++; } } /** * @inheritDoc */ public function setOrder(PiggyBank $piggyBank, int $newOrder): bool { $oldOrder = (int)$piggyBank->order; Log::debug(sprintf('Will move piggy bank #%d ("%s") from %d to %d', $piggyBank->id, $piggyBank->name, $oldOrder, $newOrder)); if ($newOrder > $oldOrder) { $this->user->piggyBanks()->where('piggy_banks.order', '<=', $newOrder)->where('piggy_banks.order', '>', $oldOrder) ->where('piggy_banks.id', '!=', $piggyBank->id) ->decrement('piggy_banks.order'); $piggyBank->order = $newOrder; Log::debug(sprintf('Order of piggy #%d ("%s") is now %d', $piggyBank->id, $piggyBank->name, $newOrder)); $piggyBank->save(); return true; } $this->user->piggyBanks()->where('piggy_banks.order', '>=', $newOrder)->where('piggy_banks.order', '<', $oldOrder) ->where('piggy_banks.id', '!=', $piggyBank->id) ->increment('piggy_banks.order'); $piggyBank->order = $newOrder; Log::debug(sprintf('Order of piggy #%d ("%s") is now %d', $piggyBank->id, $piggyBank->name, $newOrder)); $piggyBank->save(); return true; } /** * @param PiggyBank $piggyBank * @param string $note * * @return bool */ private function updateNote(PiggyBank $piggyBank, string $note): bool { if ('' === $note) { $dbNote = $piggyBank->notes()->first(); if (null !== $dbNote) { try { $dbNote->delete(); } catch (Exception $e) { // @ignoreException } } return true; } $dbNote = $piggyBank->notes()->first(); if (null === $dbNote) { $dbNote = new Note(); $dbNote->noteable()->associate($piggyBank); } $dbNote->text = trim($note); $dbNote->save(); return true; } /** * @param PiggyBank $piggyBank * @param array $data * * @return PiggyBank */ public function update(PiggyBank $piggyBank, array $data): PiggyBank { $piggyBank = $this->updateProperties($piggyBank, $data); if (array_key_exists('notes', $data)) { $this->updateNote($piggyBank, (string)$data['notes']); } // update the order of the piggy bank: $oldOrder = (int)$piggyBank->order; $newOrder = (int)($data['order'] ?? $oldOrder); if ($oldOrder !== $newOrder) { $this->setOrder($piggyBank, $newOrder); } // if the piggy bank is now smaller than the current relevant rep, // remove money from the rep. $repetition = $this->getRepetition($piggyBank); if (null !== $repetition && $repetition->currentamount > $piggyBank->targetamount && 0 !== bccomp($piggyBank->targetamount, '0')) { $difference = bcsub($piggyBank->targetamount, $repetition->currentamount); // an amount will be removed, create "negative" event: event(new ChangedPiggyBankAmount($piggyBank, $difference, null, null)); $repetition->currentamount = $piggyBank->targetamount; $repetition->save(); } // update using name: if (array_key_exists('object_group_title', $data)) { $objectGroupTitle = (string)$data['object_group_title']; if ('' !== $objectGroupTitle) { $objectGroup = $this->findOrCreateObjectGroup($objectGroupTitle); if (null !== $objectGroup) { $piggyBank->objectGroups()->sync([$objectGroup->id]); $piggyBank->save(); } return $piggyBank; } // remove if name is empty. Should be overruled by ID. if ('' === $objectGroupTitle) { $piggyBank->objectGroups()->sync([]); $piggyBank->save(); } } // try also with ID: if (array_key_exists('object_group_id', $data)) { $objectGroupId = (int)($data['object_group_id'] ?? 0); if (0 !== $objectGroupId) { $objectGroup = $this->findObjectGroupById($objectGroupId); if (null !== $objectGroup) { $piggyBank->objectGroups()->sync([$objectGroup->id]); $piggyBank->save(); } return $piggyBank; } } return $piggyBank; } /** * @param PiggyBank $piggyBank * @param array $data * * @return PiggyBank */ private function updateProperties(PiggyBank $piggyBank, array $data): PiggyBank { if (array_key_exists('name', $data) && '' !== $data['name']) { $piggyBank->name = $data['name']; } if (array_key_exists('account_id', $data) && 0 !== $data['account_id']) { $piggyBank->account_id = (int)$data['account_id']; } if (array_key_exists('targetamount', $data) && '' !== $data['targetamount']) { $piggyBank->targetamount = $data['targetamount']; } if (array_key_exists('targetamount', $data) && '' === $data['targetamount']) { $piggyBank->targetamount = '0'; } if (array_key_exists('targetdate', $data) && '' !== $data['targetdate']) { $piggyBank->targetdate = $data['targetdate']; } if (array_key_exists('startdate', $data)) { $piggyBank->startdate = $data['startdate']; } $piggyBank->save(); return $piggyBank; } }