mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2025-01-26 16:26:35 -06:00
First attempt to fix #7589
This commit is contained in:
parent
2e7a17560d
commit
463ebd296f
@ -141,6 +141,11 @@ class UpdateRequest extends FormRequest
|
||||
function (Validator $validator) {
|
||||
//$this->validateOneRecurrenceTransaction($validator);
|
||||
//$this->validateOneRepetitionUpdate($validator);
|
||||
|
||||
|
||||
/** @var Recurrence $recurrence */
|
||||
$recurrence = $this->route()->parameter('recurrence');
|
||||
$this->validateTransactionId($recurrence, $validator);
|
||||
$this->validateRecurrenceRepetition($validator);
|
||||
$this->validateRepetitionMoment($validator);
|
||||
$this->validateForeignCurrencyInformation($validator);
|
||||
@ -212,4 +217,5 @@ class UpdateRequest extends FormRequest
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -352,8 +352,6 @@ trait RecurringTransactionTrait
|
||||
/**
|
||||
* @param RecurrenceTransaction $transaction
|
||||
* @param int $categoryId
|
||||
*
|
||||
* @throws FireflyException
|
||||
*/
|
||||
private function setCategory(RecurrenceTransaction $transaction, int $categoryId): void
|
||||
{
|
||||
|
@ -107,13 +107,22 @@ class RecurrenceUpdateService
|
||||
// update all transactions (and associated meta-data)
|
||||
if (array_key_exists('transactions', $data)) {
|
||||
$this->updateTransactions($recurrence, $data['transactions'] ?? []);
|
||||
// $this->deleteTransactions($recurrence);
|
||||
// $this->createTransactions($recurrence, $data['transactions'] ?? []);
|
||||
}
|
||||
|
||||
return $recurrence;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Recurrence $recurrence
|
||||
* @param int $transactionId
|
||||
* @return void
|
||||
*/
|
||||
private function deleteTransaction(Recurrence $recurrence, int $transactionId): void
|
||||
{
|
||||
Log::debug(sprintf('Will delete transaction #%d in recurrence #%d.', $transactionId, $recurrence->id));
|
||||
$recurrence->recurrenceTransactions()->where('id', $transactionId)->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Recurrence $recurrence
|
||||
* @param array $data
|
||||
@ -146,42 +155,6 @@ class RecurrenceUpdateService
|
||||
return $query->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Recurrence $recurrence
|
||||
* @param array $data
|
||||
*
|
||||
* @return RecurrenceTransaction|null
|
||||
*/
|
||||
private function matchTransaction(Recurrence $recurrence, array $data): ?RecurrenceTransaction
|
||||
{
|
||||
Log::debug('Now in matchTransaction()');
|
||||
$originalCount = $recurrence->recurrenceTransactions()->count();
|
||||
if (1 === $originalCount) {
|
||||
Log::debug('Return the first one.');
|
||||
/** @var RecurrenceTransaction|null */
|
||||
return $recurrence->recurrenceTransactions()->first();
|
||||
}
|
||||
// find it based on data
|
||||
$fields = [
|
||||
'id' => 'id',
|
||||
'currency_id' => 'transaction_currency_id',
|
||||
'foreign_currency_id' => 'foreign_currency_id',
|
||||
'source_id' => 'source_id',
|
||||
'destination_id' => 'destination_id',
|
||||
'amount' => 'amount',
|
||||
'foreign_amount' => 'foreign_amount',
|
||||
'description' => 'description',
|
||||
];
|
||||
$query = $recurrence->recurrenceTransactions();
|
||||
foreach ($fields as $field => $column) {
|
||||
if (array_key_exists($field, $data)) {
|
||||
$query->where($column, $data[$field]);
|
||||
}
|
||||
}
|
||||
/** @var RecurrenceTransaction|null */
|
||||
return $query->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Recurrence $recurrence
|
||||
* @param string $text
|
||||
@ -202,6 +175,87 @@ class RecurrenceUpdateService
|
||||
$dbNote?->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Recurrence $recurrence
|
||||
* @param array $combination
|
||||
* @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']);
|
||||
Log::debug(sprintf('Now in updateCombination(#%d)', $original['id']));
|
||||
|
||||
$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)) {
|
||||
$currency = $currencyFactory->find($submitted['currency_id'] ?? null, $currency['currency_code'] ?? null);
|
||||
}
|
||||
if (null === $currency) {
|
||||
unset($submitted['currency_id'], $submitted['currency_code']);
|
||||
}
|
||||
if (null !== $currency) {
|
||||
$submitted['currency_id'] = (int)$currency->id;
|
||||
}
|
||||
if (array_key_exists('foreign_currency_id', $submitted) || array_key_exists('foreign_currency_code', $submitted)) {
|
||||
$foreignCurrency = $currencyFactory->find($submitted['foreign_currency_id'] ?? null, $currency['foreign_currency_code'] ?? null);
|
||||
}
|
||||
if (null === $foreignCurrency) {
|
||||
unset($submitted['foreign_currency_id'], $currency['foreign_currency_code']);
|
||||
}
|
||||
if (null !== $foreignCurrency) {
|
||||
$submitted['foreign_currency_id'] = (int)$foreignCurrency->id;
|
||||
}
|
||||
|
||||
// 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']) {
|
||||
Log::debug('Category name is submitted but is empty. Set category to be empty.');
|
||||
$submitted['category_name'] = null;
|
||||
$submitted['category_id'] = 0;
|
||||
}
|
||||
|
||||
if (array_key_exists('category_id', $submitted)) {
|
||||
Log::debug(sprintf('Category ID is submitted, set category to be %d.', (int)$submitted['category_id']));
|
||||
$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']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param Recurrence $recurrence
|
||||
@ -213,7 +267,7 @@ class RecurrenceUpdateService
|
||||
{
|
||||
$originalCount = $recurrence->recurrenceRepetitions()->count();
|
||||
if (0 === count($repetitions)) {
|
||||
// wont drop repetition, rather avoid.
|
||||
// won't drop repetition, rather avoid.
|
||||
return;
|
||||
}
|
||||
// user added or removed repetitions, delete all and recreate:
|
||||
@ -251,98 +305,60 @@ class RecurrenceUpdateService
|
||||
*
|
||||
* @param Recurrence $recurrence
|
||||
* @param array $transactions
|
||||
*
|
||||
* @throws FireflyException
|
||||
* @throws JsonException
|
||||
*/
|
||||
private function updateTransactions(Recurrence $recurrence, array $transactions): void
|
||||
{
|
||||
Log::debug('Now in updateTransactions()');
|
||||
$originalCount = $recurrence->recurrenceTransactions()->count();
|
||||
Log::debug(sprintf('Original count is %d', $originalCount));
|
||||
if (0 === count($transactions)) {
|
||||
// won't drop transactions, rather avoid.
|
||||
Log::warning('No transactions to update, too scared to continue!');
|
||||
return;
|
||||
}
|
||||
// user added or removed repetitions, delete all and recreate:
|
||||
if ($originalCount !== count($transactions)) {
|
||||
Log::debug('Delete existing transactions and create new ones.');
|
||||
$this->deleteTransactions($recurrence);
|
||||
$this->createTransactions($recurrence, $transactions);
|
||||
|
||||
return;
|
||||
}
|
||||
$currencyFactory = app(TransactionCurrencyFactory::class);
|
||||
// loop all and try to match them:
|
||||
Log::debug(sprintf('Count is equal (%d), update transactions.', $originalCount));
|
||||
foreach ($transactions as $current) {
|
||||
$match = $this->matchTransaction($recurrence, $current);
|
||||
if (null === $match) {
|
||||
throw new FireflyException('Cannot match recurring transaction to existing transaction. Not sure what to do. Break.');
|
||||
}
|
||||
// complex loop to find currency:
|
||||
$currency = null;
|
||||
$foreignCurrency = null;
|
||||
if (array_key_exists('currency_id', $current) || array_key_exists('currency_code', $current)) {
|
||||
$currency = $currencyFactory->find($current['currency_id'] ?? null, $currency['currency_code'] ?? null);
|
||||
}
|
||||
if (null === $currency) {
|
||||
unset($current['currency_id'], $current['currency_code']);
|
||||
}
|
||||
if (null !== $currency) {
|
||||
$current['currency_id'] = (int)$currency->id;
|
||||
}
|
||||
if (array_key_exists('foreign_currency_id', $current) || array_key_exists('foreign_currency_code', $current)) {
|
||||
$foreignCurrency = $currencyFactory->find($current['foreign_currency_id'] ?? null, $currency['foreign_currency_code'] ?? null);
|
||||
}
|
||||
if (null === $foreignCurrency) {
|
||||
unset($current['foreign_currency_id'], $currency['foreign_currency_code']);
|
||||
}
|
||||
if (null !== $foreignCurrency) {
|
||||
$current['foreign_currency_id'] = (int)$foreignCurrency->id;
|
||||
}
|
||||
|
||||
// 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, $current)) {
|
||||
$match->$column = $current[$field];
|
||||
$match->save();
|
||||
$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']) {
|
||||
Log::debug(sprintf('Match original transaction #%d with an entry in the submitted array.', $originalTransaction['id']));
|
||||
$combinations[] = [
|
||||
'original' => $originalTransaction,
|
||||
'submitted' => $submittedTransaction,
|
||||
];
|
||||
unset($originalTransactions[$i]);
|
||||
unset($transactions[$ii]);
|
||||
}
|
||||
}
|
||||
// update meta data
|
||||
if (array_key_exists('budget_id', $current)) {
|
||||
$this->setBudget($match, (int)$current['budget_id']);
|
||||
}
|
||||
if (array_key_exists('bill_id', $current)) {
|
||||
$this->setBill($match, (int)$current['bill_id']);
|
||||
}
|
||||
// reset category if name is set but empty:
|
||||
// can be removed when v1 is retired.
|
||||
if (array_key_exists('category_name', $current) && '' === (string)$current['category_name']) {
|
||||
Log::debug('Category name is submitted but is empty. Set category to be empty.');
|
||||
$current['category_name'] = null;
|
||||
$current['category_id'] = 0;
|
||||
}
|
||||
|
||||
if (array_key_exists('category_id', $current)) {
|
||||
Log::debug(sprintf('Category ID is submitted, set category to be %d.', (int)$current['category_id']));
|
||||
$this->setCategory($match, (int)$current['category_id']);
|
||||
}
|
||||
|
||||
if (array_key_exists('tags', $current) && is_array($current['tags'])) {
|
||||
$this->updateTags($match, $current['tags']);
|
||||
}
|
||||
if (array_key_exists('piggy_bank_id', $current)) {
|
||||
$this->updatePiggyBank($match, (int)$current['piggy_bank_id']);
|
||||
}
|
||||
/**
|
||||
* If one left of both we can match those as well and presto.
|
||||
*/
|
||||
if (1 === count($originalTransactions) && 1 === count($transactions)) {
|
||||
$first = array_shift($originalTransactions);
|
||||
Log::debug(sprintf('One left of each, link them (ID is #%d)', $first['id']));
|
||||
$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) {
|
||||
Log::debug(sprintf('Original transaction #%d is unmatched, delete it!', $original['id']));
|
||||
$this->deleteTransaction($recurrence, (int)$original['id']);
|
||||
}
|
||||
// anything left is new.
|
||||
$this->createTransactions($recurrence, $transactions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,10 @@ trait GetRecurrenceData
|
||||
{
|
||||
$return = [];
|
||||
|
||||
if (array_key_exists('id', $transaction)) {
|
||||
$return['id'] = (string)$transaction['id'];
|
||||
}
|
||||
|
||||
// amount + currency
|
||||
if (array_key_exists('amount', $transaction)) {
|
||||
$return['amount'] = $transaction['amount'];
|
||||
|
@ -271,6 +271,7 @@ class RecurrenceTransformer extends AbstractTransformer
|
||||
$foreignAmount = app('steam')->bcround($transaction->foreign_amount, $foreignCurrencyDp);
|
||||
}
|
||||
$transactionArray = [
|
||||
'id' => (string)$transaction->id,
|
||||
'currency_id' => (string)$transaction->transaction_currency_id,
|
||||
'currency_code' => $transaction->transactionCurrency->code,
|
||||
'currency_symbol' => $transaction->transactionCurrency->symbol,
|
||||
|
@ -290,6 +290,105 @@ trait RecurrenceValidation
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Validator $validator
|
||||
* @return void
|
||||
*/
|
||||
protected function validateTransactionId(Recurrence $recurrence, Validator $validator): void
|
||||
{
|
||||
Log::debug('Now in validateTransactionId');
|
||||
$transactions = $this->getTransactionData();
|
||||
$submittedTrCount = count($transactions);
|
||||
|
||||
//$recurrence = $validator->get
|
||||
if (null === $transactions) {
|
||||
Log::warning('[a] User submitted no transactions.');
|
||||
$validator->errors()->add('transactions', (string)trans('validation.at_least_one_transaction'));
|
||||
return;
|
||||
}
|
||||
if (0 === $submittedTrCount) {
|
||||
Log::warning('[b] User submitted no transactions.');
|
||||
$validator->errors()->add('transactions', (string)trans('validation.at_least_one_transaction'));
|
||||
return;
|
||||
}
|
||||
$originalTrCount = $recurrence->recurrenceTransactions()->count();
|
||||
if (1 === $submittedTrCount && 1 === $originalTrCount) {
|
||||
$first = $transactions[0]; // can safely assume index 0.
|
||||
if (!array_key_exists('id', $first)) {
|
||||
Log::debug('Single count and no ID, done.');
|
||||
return; // home safe!
|
||||
}
|
||||
$id = $first['id'];
|
||||
if ('' === (string)$id) {
|
||||
Log::debug('Single count and empty ID, done.');
|
||||
return; // home safe!
|
||||
}
|
||||
$integer = (int)$id;
|
||||
$secondCount = $recurrence->recurrenceTransactions()->where('recurrences_transactions.id', $integer)->count();
|
||||
Log::debug(sprintf('Result of ID count: %d', $secondCount));
|
||||
if (0 === $secondCount) {
|
||||
$validator->errors()->add('transactions.0.id', (string)trans('validation.id_does_not_match', ['id' => $integer]));
|
||||
}
|
||||
Log::debug('Single ID validation done.');
|
||||
return;
|
||||
}
|
||||
|
||||
Log::debug('Multi ID validation.');
|
||||
$idsMandatory = false;
|
||||
if ($submittedTrCount < $originalTrCount) {
|
||||
Log::debug(sprintf('User submits %d transaction, recurrence has %d transactions. All entries must have ID.', $submittedTrCount, $originalTrCount));
|
||||
$idsMandatory = true;
|
||||
}
|
||||
/**
|
||||
* Loop all transactions submitted by the user.
|
||||
* If the user has submitted fewer transactions than the original recurrence has, all submitted entries must have an ID.
|
||||
* Any ID's missing will be deleted later on.
|
||||
*
|
||||
* If the user submits more or the same number of transactions (n), the following rules apply:
|
||||
*
|
||||
* 1. Any 1 transaction does not need to have an ID. Since the other n-1 can be matched, the last one can be assumed.
|
||||
* 2. If the user submits more transactions than already present, count the number of existing transactions. At least those must be matched. After that, submit as many as you like.
|
||||
* 3. If the user submits the same number of transactions as already present, all but one must have an ID.
|
||||
*/
|
||||
$unmatchedIds = 0;
|
||||
|
||||
foreach ($transactions as $index => $transaction) {
|
||||
Log::debug(sprintf('Now at %d/%d', $index + 1, $submittedTrCount));
|
||||
if (!is_array($transaction)) {
|
||||
Log::warning('Not an array. Give error.');
|
||||
$validator->errors()->add(sprintf('transactions.%d.id', $index), (string)trans('validation.at_least_one_transaction'));
|
||||
return;
|
||||
}
|
||||
if (!array_key_exists('id', $transaction) && $idsMandatory) {
|
||||
Log::warning('ID is mandatory but array has no ID.');
|
||||
$validator->errors()->add(sprintf('transactions.%d.id', $index), (string)trans('validation.need_id_to_match'));
|
||||
return;
|
||||
}
|
||||
if (array_key_exists('id', $transaction)) { // don't matter if $idsMandatory
|
||||
Log::debug('Array has ID.');
|
||||
$idCount = $recurrence->recurrenceTransactions()->where('recurrences_transactions.id', (int)$transaction['id'])->count();
|
||||
if (0 === $idCount) {
|
||||
Log::debug('ID does not exist or no match. Count another unmatched ID.');
|
||||
$unmatchedIds++;
|
||||
}
|
||||
}
|
||||
if (!array_key_exists('id', $transaction) && !$idsMandatory) {
|
||||
Log::debug('Array has no ID but was not mandatory at this point.');
|
||||
$unmatchedIds++;
|
||||
}
|
||||
}
|
||||
// if too many don't match, but you haven't submitted more than already present:
|
||||
$maxUnmatched = max(1, $submittedTrCount - $originalTrCount);
|
||||
Log::debug(sprintf('Submitted: %d. Original: %d. User can submit %d unmatched transactions.', $submittedTrCount, $originalTrCount, $maxUnmatched));
|
||||
if ($unmatchedIds > $maxUnmatched) {
|
||||
Log::warning(sprintf('Too many unmatched transactions (%d).', $unmatchedIds));
|
||||
$validator->errors()->add('transactions.0.id', (string)trans('validation.too_many_unmatched'));
|
||||
return;
|
||||
}
|
||||
Log::debug('Done with ID validation.');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* If the repetition type is weekly, the moment should be a day between 1-7 (inclusive).
|
||||
*
|
||||
|
@ -51,6 +51,10 @@ return [
|
||||
'invalid_selection' => 'Your selection is invalid.',
|
||||
'belongs_user' => 'This value is invalid for this field.',
|
||||
'at_least_one_transaction' => 'Need at least one transaction.',
|
||||
'recurring_transaction_id' => 'Need at least one transaction.',
|
||||
'need_id_to_match' => 'You need to submit this entry with an ID for the API to be able to match it.',
|
||||
'too_many_unmatched' => 'Too many submitted transactions cannot be matched to their respective database entries. Make sure existing entries have a valid ID.',
|
||||
'id_does_not_match' => 'Submitted ID #:id does not match expected ID. Make sure it matches or omit the field.',
|
||||
'at_least_one_repetition' => 'Need at least one repetition.',
|
||||
'require_repeat_until' => 'Require either a number of repetitions, or an end date (repeat_until). Not both.',
|
||||
'require_currency_info' => 'The content of this field is invalid without currency information.',
|
||||
|
Loading…
Reference in New Issue
Block a user