Files
firefly-iii/app/Services/Internal/Update/RecurrenceUpdateService.php

374 lines
14 KiB
PHP
Raw Normal View History

<?php
/**
* RecurrenceUpdateService.php
2020-02-16 13:56:35 +01:00
* Copyright (c) 2019 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Internal\Update;
use FireflyIII\Exceptions\FireflyException;
2021-03-15 19:51:55 +01:00
use FireflyIII\Factory\TransactionCurrencyFactory;
2019-08-27 05:57:58 +02:00
use FireflyIII\Models\Note;
use FireflyIII\Models\Recurrence;
2021-03-12 18:31:19 +01:00
use FireflyIII\Models\RecurrenceRepetition;
2021-03-15 08:51:21 +01:00
use FireflyIII\Models\RecurrenceTransaction;
use FireflyIII\Services\Internal\Support\RecurringTransactionTrait;
use FireflyIII\Services\Internal\Support\TransactionTypeTrait;
use FireflyIII\User;
2023-07-15 16:02:42 +02:00
use JsonException;
/**
* Class RecurrenceUpdateService
*
*/
class RecurrenceUpdateService
{
2022-10-30 14:24:37 +01:00
use RecurringTransactionTrait;
2023-11-04 14:18:49 +01:00
use TransactionTypeTrait;
2020-10-26 19:15:57 +01:00
private User $user;
/**
* Updates a recurrence.
*
2022-10-30 11:43:17 +01:00
* TODO if the user updates the type, the accounts must be validated again.
2019-08-27 05:57:58 +02:00
*
2023-06-21 12:34:58 +02:00
* @param Recurrence $recurrence
* @param array $data
*
* @return Recurrence
* @throws FireflyException
*/
public function update(Recurrence $recurrence, array $data): Recurrence
{
2021-03-12 18:31:19 +01:00
$this->user = $recurrence->user;
// update basic fields first:
2019-01-27 17:09:39 +01:00
2021-03-12 18:31:19 +01:00
if (array_key_exists('recurrence', $data)) {
$info = $data['recurrence'];
if (array_key_exists('title', $info)) {
$recurrence->title = $info['title'];
}
if (array_key_exists('description', $info)) {
$recurrence->description = $info['description'];
}
if (array_key_exists('first_date', $info)) {
$recurrence->first_date = $info['first_date'];
}
if (array_key_exists('repeat_until', $info)) {
$recurrence->repeat_until = $info['repeat_until'];
$recurrence->repetitions = 0;
}
2021-03-12 18:31:19 +01:00
if (array_key_exists('nr_of_repetitions', $info)) {
2022-12-29 19:42:26 +01:00
if (0 !== (int)$info['nr_of_repetitions']) {
2021-03-12 18:31:19 +01:00
$recurrence->repeat_until = null;
}
$recurrence->repetitions = $info['nr_of_repetitions'];
}
if (array_key_exists('apply_rules', $info)) {
$recurrence->apply_rules = $info['apply_rules'];
}
if (array_key_exists('active', $info)) {
$recurrence->active = $info['active'];
}
// update all meta data:
if (array_key_exists('notes', $info)) {
$this->setNoteText($recurrence, $info['notes']);
}
}
$recurrence->save();
// update all repetitions
2021-03-12 18:31:19 +01:00
if (array_key_exists('repetitions', $data)) {
2023-10-29 06:33:43 +01:00
app('log')->debug('Will update repetitions array');
2021-03-12 18:31:19 +01:00
// update each repetition or throw error yay
$this->updateRepetitions($recurrence, $data['repetitions'] ?? []);
}
2021-03-12 20:25:15 +01:00
// update all transactions:
2021-03-15 08:51:21 +01:00
// update all transactions (and associated meta-data)
if (array_key_exists('transactions', $data)) {
$this->updateTransactions($recurrence, $data['transactions'] ?? []);
}
return $recurrence;
}
2019-08-27 05:57:58 +02:00
2023-06-03 21:17:49 +02:00
/**
2023-06-21 12:34:58 +02:00
* @param Recurrence $recurrence
* @param string $text
2023-06-03 21:17:49 +02:00
*/
2023-06-21 12:34:58 +02:00
private function setNoteText(Recurrence $recurrence, string $text): void
2023-06-03 21:17:49 +02:00
{
2023-06-21 12:34:58 +02:00
$dbNote = $recurrence->notes()->first();
if ('' !== $text) {
if (null === $dbNote) {
$dbNote = new Note();
$dbNote->noteable()->associate($recurrence);
}
$dbNote->text = trim($text);
$dbNote->save();
return;
}
$dbNote?->delete();
2023-06-03 21:17:49 +02:00
}
2023-05-29 13:56:55 +02:00
/**
2023-06-21 12:34:58 +02:00
*
* @param Recurrence $recurrence
* @param array $repetitions
*
* @throws FireflyException
*/
private function updateRepetitions(Recurrence $recurrence, array $repetitions): void
{
$originalCount = $recurrence->recurrenceRepetitions()->count();
if (0 === count($repetitions)) {
// won't drop repetition, rather avoid.
return;
}
// user added or removed repetitions, delete all and recreate:
if ($originalCount !== count($repetitions)) {
2023-10-29 06:33:43 +01:00
app('log')->debug('Delete existing repetitions and create new ones.');
2023-06-21 12:34:58 +02:00
$this->deleteRepetitions($recurrence);
$this->createRepetitions($recurrence, $repetitions);
return;
}
// loop all and try to match them:
2023-10-29 06:33:43 +01:00
app('log')->debug('Loop and find');
2023-06-21 12:34:58 +02:00
foreach ($repetitions as $current) {
$match = $this->matchRepetition($recurrence, $current);
if (null === $match) {
throw new FireflyException('Cannot match recurring repetition to existing repetition. Not sure what to do. Break.');
}
$fields = [
'type' => 'repetition_type',
'moment' => 'repetition_moment',
'skip' => 'repetition_skip',
'weekend' => 'weekend',
];
foreach ($fields as $field => $column) {
if (array_key_exists($field, $current)) {
$match->$column = $current[$field];
$match->save();
}
}
}
}
/**
* @param Recurrence $recurrence
* @param array $data
2023-05-29 13:56:55 +02:00
*
* @return RecurrenceRepetition|null
*/
private function matchRepetition(Recurrence $recurrence, array $data): ?RecurrenceRepetition
{
$originalCount = $recurrence->recurrenceRepetitions()->count();
if (1 === $originalCount) {
2023-10-29 06:33:43 +01:00
app('log')->debug('Return the first one');
/** @var RecurrenceRepetition|null */
return $recurrence->recurrenceRepetitions()->first();
2023-05-29 13:56:55 +02:00
}
// find it:
$fields = [
'id' => 'id',
'type' => 'repetition_type',
'moment' => 'repetition_moment',
'skip' => 'repetition_skip',
'weekend' => 'weekend',
];
$query = $recurrence->recurrenceRepetitions();
foreach ($fields as $field => $column) {
if (array_key_exists($field, $data)) {
$query->where($column, $data[$field]);
}
}
/** @var RecurrenceRepetition|null */
return $query->first();
}
2019-08-27 05:57:58 +02:00
/**
2023-06-21 12:34:58 +02:00
* TODO this method is very complex.
*
* @param Recurrence $recurrence
2023-07-15 16:02:42 +02:00
* @param array $transactions
*
2023-07-04 13:29:19 +02:00
* @throws FireflyException
2023-07-15 16:02:42 +02:00
* @throws JsonException
2019-08-27 05:57:58 +02:00
*/
2023-06-21 12:34:58 +02:00
private function updateTransactions(Recurrence $recurrence, array $transactions): void
2019-08-27 05:57:58 +02:00
{
2023-10-29 06:33:43 +01:00
app('log')->debug('Now in updateTransactions()');
2023-06-21 12:34:58 +02:00
$originalCount = $recurrence->recurrenceTransactions()->count();
2023-10-29 06:33:43 +01:00
app('log')->debug(sprintf('Original count is %d', $originalCount));
2023-06-21 12:34:58 +02:00
if (0 === count($transactions)) {
// won't drop transactions, rather avoid.
2023-10-29 06:31:13 +01:00
app('log')->warning('No transactions to update, too scared to continue!');
2019-08-27 05:57:58 +02:00
return;
}
2023-06-21 12:34:58 +02:00
$combinations = [];
$originalTransactions = $recurrence->recurrenceTransactions()->get()->toArray();
/**
* First, make sure to loop all existing transactions and match them to a counterpart in the submitted transactions array.
*/
foreach ($originalTransactions as $i => $originalTransaction) {
foreach ($transactions as $ii => $submittedTransaction) {
if (array_key_exists('id', $submittedTransaction) && (int)$originalTransaction['id'] === (int)$submittedTransaction['id']) {
2023-10-29 06:33:43 +01:00
app('log')->debug(sprintf('Match original transaction #%d with an entry in the submitted array.', $originalTransaction['id']));
2023-06-21 12:34:58 +02:00
$combinations[] = [
'original' => $originalTransaction,
'submitted' => $submittedTransaction,
];
unset($originalTransactions[$i]);
unset($transactions[$ii]);
}
}
}
/**
* If one left of both we can match those as well and presto.
*/
if (1 === count($originalTransactions) && 1 === count($transactions)) {
$first = array_shift($originalTransactions);
2023-10-29 06:33:43 +01:00
app('log')->debug(sprintf('One left of each, link them (ID is #%d)', $first['id']));
2023-06-21 12:34:58 +02:00
$combinations[] = [
'original' => $first,
'submitted' => array_shift($transactions),
];
unset($first);
}
// if they are both empty, we can safely loop all combinations and update them.
if (0 === count($originalTransactions) && 0 === count($transactions)) {
foreach ($combinations as $combination) {
$this->updateCombination($recurrence, $combination);
}
}
// anything left in the original transactions array can be deleted.
foreach ($originalTransactions as $original) {
2023-10-29 06:33:43 +01:00
app('log')->debug(sprintf('Original transaction #%d is unmatched, delete it!', $original['id']));
2023-06-21 12:34:58 +02:00
$this->deleteTransaction($recurrence, (int)$original['id']);
}
// anything left is new.
$this->createTransactions($recurrence, $transactions);
2019-08-27 05:57:58 +02:00
}
2021-03-12 18:31:19 +01:00
2023-06-03 21:17:49 +02:00
/**
2023-06-21 12:34:58 +02:00
* @param Recurrence $recurrence
* @param array $combination
2023-07-15 16:02:42 +02:00
*
2023-06-03 21:17:49 +02:00
* @return void
*/
private function updateCombination(Recurrence $recurrence, array $combination): void
{
$original = $combination['original'];
$submitted = $combination['submitted'];
/** @var RecurrenceTransaction $transaction */
$transaction = $recurrence->recurrenceTransactions()->find($original['id']);
2023-10-29 06:33:43 +01:00
app('log')->debug(sprintf('Now in updateCombination(#%d)', $original['id']));
2023-06-03 21:17:49 +02:00
$currencyFactory = app(TransactionCurrencyFactory::class);
// loop all and try to match them:
$currency = null;
$foreignCurrency = null;
if (array_key_exists('currency_id', $submitted) || array_key_exists('currency_code', $submitted)) {
2023-11-04 07:18:03 +01:00
$currency = $currencyFactory->find(
array_key_exists('currency_id', $submitted) ? (int)$submitted['currency_id'] : null,
2023-11-04 14:09:51 +01:00
array_key_exists('currency_code', $submitted) ? $submitted['currency_code'] : null
);
2023-06-03 21:17:49 +02:00
}
if (null === $currency) {
unset($submitted['currency_id'], $submitted['currency_code']);
}
if (null !== $currency) {
2023-11-05 19:41:37 +01:00
$submitted['currency_id'] = $currency->id;
2023-06-03 21:17:49 +02:00
}
if (array_key_exists('foreign_currency_id', $submitted) || array_key_exists('foreign_currency_code', $submitted)) {
2023-11-04 07:18:03 +01:00
$foreignCurrency = $currencyFactory->find(
array_key_exists('foreign_currency_id', $submitted) ? (int)$submitted['foreign_currency_id'] : null,
2023-11-04 14:09:51 +01:00
array_key_exists('foreign_currency_code', $submitted) ? $submitted['foreign_currency_code'] : null
);
2023-06-03 21:17:49 +02:00
}
if (null === $foreignCurrency) {
unset($submitted['foreign_currency_id'], $currency['foreign_currency_code']);
}
if (null !== $foreignCurrency) {
2023-11-05 19:41:37 +01:00
$submitted['foreign_currency_id'] = $foreignCurrency->id;
2023-06-03 21:17:49 +02:00
}
// update fields that are part of the recurring transaction itself.
$fields = [
'source_id' => 'source_id',
'destination_id' => 'destination_id',
'amount' => 'amount',
'foreign_amount' => 'foreign_amount',
'description' => 'description',
'currency_id' => 'transaction_currency_id',
'foreign_currency_id' => 'foreign_currency_id',
];
foreach ($fields as $field => $column) {
if (array_key_exists($field, $submitted)) {
$transaction->$column = $submitted[$field];
$transaction->save();
}
}
// update meta data
if (array_key_exists('budget_id', $submitted)) {
$this->setBudget($transaction, (int)$submitted['budget_id']);
}
if (array_key_exists('bill_id', $submitted)) {
$this->setBill($transaction, (int)$submitted['bill_id']);
}
// reset category if name is set but empty:
// can be removed when v1 is retired.
if (array_key_exists('category_name', $submitted) && '' === (string)$submitted['category_name']) {
2023-10-29 06:33:43 +01:00
app('log')->debug('Category name is submitted but is empty. Set category to be empty.');
2023-06-03 21:17:49 +02:00
$submitted['category_name'] = null;
$submitted['category_id'] = 0;
}
if (array_key_exists('category_id', $submitted)) {
2023-10-29 06:33:43 +01:00
app('log')->debug(sprintf('Category ID is submitted, set category to be %d.', (int)$submitted['category_id']));
2023-06-03 21:17:49 +02:00
$this->setCategory($transaction, (int)$submitted['category_id']);
}
if (array_key_exists('tags', $submitted) && is_array($submitted['tags'])) {
$this->updateTags($transaction, $submitted['tags']);
}
if (array_key_exists('piggy_bank_id', $submitted)) {
$this->updatePiggyBank($transaction, (int)$submitted['piggy_bank_id']);
}
}
2021-03-12 18:31:19 +01:00
/**
2023-06-21 12:34:58 +02:00
* @param Recurrence $recurrence
* @param int $transactionId
2023-07-15 16:02:42 +02:00
*
2023-06-21 12:34:58 +02:00
* @return void
2021-03-15 10:31:11 +01:00
*/
2023-06-21 12:34:58 +02:00
private function deleteTransaction(Recurrence $recurrence, int $transactionId): void
2021-03-15 10:31:11 +01:00
{
2023-10-29 06:33:43 +01:00
app('log')->debug(sprintf('Will delete transaction #%d in recurrence #%d.', $transactionId, $recurrence->id));
2023-06-21 12:34:58 +02:00
$recurrence->recurrenceTransactions()->where('id', $transactionId)->delete();
2021-03-15 10:31:11 +01:00
}
}