diff --git a/app/Factory/TransactionJournalFactory.php b/app/Factory/TransactionJournalFactory.php index ca6f010fb0..4670c770d2 100644 --- a/app/Factory/TransactionJournalFactory.php +++ b/app/Factory/TransactionJournalFactory.php @@ -56,12 +56,9 @@ class TransactionJournalFactory { use JournalServiceTrait; - /** @var AccountRepositoryInterface */ - private $accountRepository; - /** @var AccountValidator */ - private $accountValidator; - /** @var BillRepositoryInterface */ - private $billRepository; + private AccountRepositoryInterface $accountRepository; + private AccountValidator $accountValidator; + private BillRepositoryInterface $billRepository; /** @var CurrencyRepositoryInterface */ private $currencyRepository; /** @var bool */ @@ -88,6 +85,7 @@ class TransactionJournalFactory public function __construct() { $this->errorOnHash = false; + // TODO move valid meta fields to config. $this->fields = [ // sepa 'sepa_cc', 'sepa_ct_op', 'sepa_ct_id', @@ -100,7 +98,11 @@ class TransactionJournalFactory // others 'recurrence_id', 'internal_reference', 'bunq_payment_id', - 'import_hash', 'import_hash_v2', 'external_id', 'original_source']; + 'import_hash', 'import_hash_v2', 'external_id', 'original_source', + + // recurring transactions + 'recurrence_total', 'recurrence_count' + ]; if ('testing' === config('app.env')) { diff --git a/app/Jobs/CreateRecurringTransactions.php b/app/Jobs/CreateRecurringTransactions.php index 967df4c2f2..374e016530 100644 --- a/app/Jobs/CreateRecurringTransactions.php +++ b/app/Jobs/CreateRecurringTransactions.php @@ -53,15 +53,15 @@ class CreateRecurringTransactions implements ShouldQueue use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; /** @var int Transaction groups created */ - public $created; + public int $created; /** @var int Number of recurrences actually fired */ - public $executed; + public int $executed; /** @var int Number of recurrences submitted */ - public $submitted; + public int $submitted; /** @var Carbon The current date */ - private $date; + private Carbon $date; /** @var bool Force the transaction to be created no matter what. */ - private $force; + private bool $force; /** @var TransactionGroupRepositoryInterface */ private $groupRepository; /** @var JournalRepositoryInterface Journal repository */ @@ -219,19 +219,23 @@ class CreateRecurringTransactions implements ShouldQueue /** * Get transaction information from a recurring transaction. * - * @param Recurrence $recurrence - * @param Carbon $date + * @param Recurrence $recurrence + * @param RecurrenceRepetition $repetition + * @param Carbon $date * * @return array * */ - private function getTransactionData(Recurrence $recurrence, Carbon $date): array + private function getTransactionData(Recurrence $recurrence, RecurrenceRepetition $repetition, Carbon $date): array { + // total transactions expected for this recurrence: + $total = $this->repository->totalTransactions($recurrence, $repetition); + $count = $this->repository->getJournalCount($recurrence) + 1; $transactions = $recurrence->recurrenceTransactions()->get(); $return = []; /** @var RecurrenceTransaction $transaction */ foreach ($transactions as $index => $transaction) { - $single = [ + $single = [ 'type' => strtolower($recurrence->transactionType->type), 'date' => $date, 'user' => $recurrence->user_id, @@ -260,6 +264,8 @@ class CreateRecurringTransactions implements ShouldQueue 'piggy_bank_name' => null, 'bill_id' => null, 'bill_name' => null, + 'recurrence_total' => $total, + 'recurrence_count' => $count, ]; $return[] = $single; } @@ -268,17 +274,18 @@ class CreateRecurringTransactions implements ShouldQueue } /** - * @param Recurrence $recurrence - * @param Carbon $date + * @param Recurrence $recurrence + * @param RecurrenceRepetition $repetition + * @param Carbon $date * * @return TransactionGroup|null */ - private function handleOccurrence(Recurrence $recurrence, Carbon $date): ?TransactionGroup + private function handleOccurrence(Recurrence $recurrence, RecurrenceRepetition $repetition, Carbon $date): ?TransactionGroup { - Log::debug(sprintf('Now at date %s.', $date->format('Y-m-d'))); + #Log::debug(sprintf('Now at date %s.', $date->format('Y-m-d'))); $date->startOfDay(); if ($date->ne($this->date)) { - Log::debug(sprintf('%s is not today (%s)', $date->format('Y-m-d'), $this->date->format('Y-m-d'))); + #Log::debug(sprintf('%s is not today (%s)', $date->format('Y-m-d'), $this->date->format('Y-m-d'))); return null; } @@ -305,10 +312,11 @@ class CreateRecurringTransactions implements ShouldQueue $groupTitle = $first->description; // @codeCoverageIgnoreEnd } + $array = [ 'user' => $recurrence->user_id, 'group_title' => $groupTitle, - 'transactions' => $this->getTransactionData($recurrence, $date), + 'transactions' => $this->getTransactionData($recurrence, $repetition, $date), ]; /** @var TransactionGroup $group */ $group = $this->groupRepository->store($array); @@ -328,17 +336,18 @@ class CreateRecurringTransactions implements ShouldQueue /** * Check if the occurences should be executed. * - * @param Recurrence $recurrence - * @param array $occurrences + * @param Recurrence $recurrence + * @param RecurrenceRepetition $repetition + * @param array $occurrences * * @return Collection */ - private function handleOccurrences(Recurrence $recurrence, array $occurrences): Collection + private function handleOccurrences(Recurrence $recurrence, RecurrenceRepetition $repetition, array $occurrences): Collection { $collection = new Collection; /** @var Carbon $date */ foreach ($occurrences as $date) { - $result = $this->handleOccurrence($recurrence, $date); + $result = $this->handleOccurrence($recurrence, $repetition, $date); if (null !== $result) { $collection->push($result); } @@ -374,6 +383,7 @@ class CreateRecurringTransactions implements ShouldQueue $includeWeekend = clone $this->date; $includeWeekend->addDays(2); $occurrences = $this->repository->getOccurrencesInRange($repetition, $recurrence->first_date, $includeWeekend); + /* Log::debug( sprintf( 'Calculated %d occurrences between %s and %s', @@ -383,9 +393,10 @@ class CreateRecurringTransactions implements ShouldQueue ), $this->debugArray($occurrences) ); + */ unset($includeWeekend); - $result = $this->handleOccurrences($recurrence, $occurrences); + $result = $this->handleOccurrences($recurrence, $repetition, $occurrences); $collection = $collection->merge($result); } diff --git a/app/Repositories/Recurring/RecurringRepository.php b/app/Repositories/Recurring/RecurringRepository.php index d44a7cd9a3..f5f53a3d02 100644 --- a/app/Repositories/Recurring/RecurringRepository.php +++ b/app/Repositories/Recurring/RecurringRepository.php @@ -569,4 +569,28 @@ class RecurringRepository implements RecurringRepositoryInterface { $this->user->recurrences()->delete(); } + + /** + * @inheritDoc + */ + public function totalTransactions(Recurrence $recurrence, RecurrenceRepetition $repetition): int + { + // if repeat = null just return 0. + if (null === $recurrence->repeat_until && 0 === (int) $recurrence->repetitions) { + return 0; + } + // expect X transactions then stop. Return that number + if (null === $recurrence->repeat_until && 0 !== (int) $recurrence->repetitions) { + return (int) $recurrence->repetitions; + } + + // need to calculate, this depends on the repetition: + if (null !== $recurrence->repeat_until && 0 === (int) $recurrence->repetitions) { + $occurrences = $this->getOccurrencesInRange($repetition, $recurrence->first_date ?? today(), $recurrence->repeat_until); + + return count($occurrences); + } + + return 0; + } } diff --git a/app/Repositories/Recurring/RecurringRepositoryInterface.php b/app/Repositories/Recurring/RecurringRepositoryInterface.php index 751bceec01..ddca5c2b21 100644 --- a/app/Repositories/Recurring/RecurringRepositoryInterface.php +++ b/app/Repositories/Recurring/RecurringRepositoryInterface.php @@ -44,6 +44,15 @@ interface RecurringRepositoryInterface */ public function destroyAll(): void; + /** + * Calculate how many transactions are to be expected from this recurrence. + * + * @param Recurrence $recurrence + * @param RecurrenceRepetition $repetition + * @return int + */ + public function totalTransactions(Recurrence $recurrence, RecurrenceRepetition $repetition): int; + /** * Destroy a recurring transaction. * diff --git a/app/Services/Internal/Support/JournalServiceTrait.php b/app/Services/Internal/Support/JournalServiceTrait.php index cdcc7172bb..12cc232a61 100644 --- a/app/Services/Internal/Support/JournalServiceTrait.php +++ b/app/Services/Internal/Support/JournalServiceTrait.php @@ -44,14 +44,10 @@ use Log; */ trait JournalServiceTrait { - /** @var AccountRepositoryInterface */ - private $accountRepository; - /** @var BudgetRepositoryInterface */ - private $budgetRepository; - /** @var CategoryRepositoryInterface */ - private $categoryRepository; - /** @var TagFactory */ - private $tagFactory; + private AccountRepositoryInterface $accountRepository; + private BudgetRepositoryInterface $budgetRepository; + private CategoryRepositoryInterface $categoryRepository; + private TagFactory $tagFactory; /** diff --git a/app/Support/Repositories/Recurring/CalculateRangeOccurrences.php b/app/Support/Repositories/Recurring/CalculateRangeOccurrences.php index 1fa3fdbf8a..39a8858e22 100644 --- a/app/Support/Repositories/Recurring/CalculateRangeOccurrences.php +++ b/app/Support/Repositories/Recurring/CalculateRangeOccurrences.php @@ -46,11 +46,11 @@ trait CalculateRangeOccurrences { $return = []; $attempts = 0; - Log::debug('Rep is daily. Start of loop.'); + #Log::debug('Rep is daily. Start of loop.'); while ($start <= $end) { - Log::debug(sprintf('Mutator is now: %s', $start->format('Y-m-d'))); + #Log::debug(sprintf('Mutator is now: %s', $start->format('Y-m-d'))); if (0 === $attempts % $skipMod) { - Log::debug(sprintf('Attempts modulo skipmod is zero, include %s', $start->format('Y-m-d'))); + #Log::debug(sprintf('Attempts modulo skipmod is zero, include %s', $start->format('Y-m-d'))); $return[] = clone $start; } $start->addDay(); @@ -77,27 +77,27 @@ trait CalculateRangeOccurrences $return = []; $attempts = 0; $dayOfMonth = (int)$moment; - Log::debug(sprintf('Day of month in repetition is %d', $dayOfMonth)); - Log::debug(sprintf('Start is %s.', $start->format('Y-m-d'))); - Log::debug(sprintf('End is %s.', $end->format('Y-m-d'))); + #Log::debug(sprintf('Day of month in repetition is %d', $dayOfMonth)); + #Log::debug(sprintf('Start is %s.', $start->format('Y-m-d'))); + #Log::debug(sprintf('End is %s.', $end->format('Y-m-d'))); if ($start->day > $dayOfMonth) { - Log::debug('Add a month.'); + #Log::debug('Add a month.'); // day has passed already, add a month. $start->addMonth(); } - Log::debug(sprintf('Start is now %s.', $start->format('Y-m-d'))); - Log::debug('Start loop.'); + #Log::debug(sprintf('Start is now %s.', $start->format('Y-m-d'))); + #Log::debug('Start loop.'); while ($start < $end) { - Log::debug(sprintf('Mutator is now %s.', $start->format('Y-m-d'))); + #Log::debug(sprintf('Mutator is now %s.', $start->format('Y-m-d'))); $domCorrected = min($dayOfMonth, $start->daysInMonth); - Log::debug(sprintf('DoM corrected is %d', $domCorrected)); + #Log::debug(sprintf('DoM corrected is %d', $domCorrected)); $start->day = $domCorrected; - Log::debug(sprintf('Mutator is now %s.', $start->format('Y-m-d'))); - Log::debug(sprintf('$attempts %% $skipMod === 0 is %s', var_export(0 === $attempts % $skipMod, true))); - Log::debug(sprintf('$start->lte($mutator) is %s', var_export($start->lte($start), true))); - Log::debug(sprintf('$end->gte($mutator) is %s', var_export($end->gte($start), true))); + #Log::debug(sprintf('Mutator is now %s.', $start->format('Y-m-d'))); + #Log::debug(sprintf('$attempts %% $skipMod === 0 is %s', var_export(0 === $attempts % $skipMod, true))); + #Log::debug(sprintf('$start->lte($mutator) is %s', var_export($start->lte($start), true))); + #Log::debug(sprintf('$end->gte($mutator) is %s', var_export($end->gte($start), true))); if (0 === $attempts % $skipMod && $start->lte($start) && $end->gte($start)) { - Log::debug(sprintf('ADD %s to return!', $start->format('Y-m-d'))); + #Log::debug(sprintf('ADD %s to return!', $start->format('Y-m-d'))); $return[] = clone $start; } $attempts++; diff --git a/app/Transformers/TransactionGroupTransformer.php b/app/Transformers/TransactionGroupTransformer.php index a6fe5573d5..b7ae0cee6f 100644 --- a/app/Transformers/TransactionGroupTransformer.php +++ b/app/Transformers/TransactionGroupTransformer.php @@ -60,7 +60,8 @@ class TransactionGroupTransformer extends AbstractTransformer $this->metaFields = [ 'sepa_cc', 'sepa_ct_op', 'sepa_ct_id', 'sepa_db', 'sepa_country', 'sepa_ep', 'sepa_ci', 'sepa_batch_id', 'internal_reference', 'bunq_payment_id', 'import_hash_v2', - 'recurrence_id', 'external_id', 'original_source', 'external_uri' + 'recurrence_id', 'external_id', 'original_source', 'external_uri', + 'recurrence_count', 'recurrence_total', ]; $this->metaDateFields = ['interest_date', 'book_date', 'process_date', 'due_date', 'payment_date', 'invoice_date']; @@ -492,13 +493,15 @@ class TransactionGroupTransformer extends AbstractTransformer 'bill_name' => $row['bill_name'], 'reconciled' => $row['reconciled'], - 'notes' => $this->groupRepos->getNoteText((int)$row['transaction_journal_id']), - 'tags' => $this->groupRepos->getTags((int)$row['transaction_journal_id']), + 'notes' => $this->groupRepos->getNoteText((int) $row['transaction_journal_id']), + 'tags' => $this->groupRepos->getTags((int) $row['transaction_journal_id']), 'internal_reference' => $metaFieldData['internal_reference'], 'external_id' => $metaFieldData['external_id'], 'original_source' => $metaFieldData['original_source'], - 'recurrence_id' => $metaFieldData['recurrence_id'], + 'recurrence_id' => null !== $metaFieldData['recurrence_id'] ? (int) $metaFieldData['recurrence_id'] : null, + 'recurrence_total' => null !== $metaFieldData['recurrence_total'] ? (int) $metaFieldData['recurrence_total'] : null, + 'recurrence_count' => null !== $metaFieldData['recurrence_count'] ? (int) $metaFieldData['recurrence_count'] : null, 'bunq_payment_id' => $metaFieldData['bunq_payment_id'], 'external_uri' => $metaFieldData['external_uri'], 'import_hash_v2' => $metaFieldData['import_hash_v2'], @@ -520,7 +523,6 @@ class TransactionGroupTransformer extends AbstractTransformer 'invoice_date' => $metaDateData['invoice_date'] ? $metaDateData['invoice_date']->toAtomString() : null, ]; } - return $result; } } diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index c8644a20c2..ad8aa537a1 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -1637,6 +1637,7 @@ return [ 'created_withdrawals' => 'Created withdrawals', 'created_deposits' => 'Created deposits', 'created_transfers' => 'Created transfers', + 'recurring_info' => 'Recurring transaction :count / :total', 'created_from_recurrence' => 'Created from recurring transaction ":title" (#:id)', 'recurring_never_cron' => 'It seems the cron job that is necessary to support recurring transactions has never run. This is of course normal when you have just installed Firefly III, but this should be something to set up as soon as possible. Please check out the help-pages using the (?)-icon in the top right corner of the page.', 'recurring_cron_long_ago' => 'It looks like it has been more than 36 hours since the cron job to support recurring transactions has fired for the last time. Are you sure it has been set up correctly? Please check out the help-pages using the (?)-icon in the top right corner of the page.', diff --git a/resources/lang/en_US/list.php b/resources/lang/en_US/list.php index 7e6943fe56..e3810b0f80 100644 --- a/resources/lang/en_US/list.php +++ b/resources/lang/en_US/list.php @@ -37,6 +37,7 @@ return [ 'linked_to_rules' => 'Relevant rules', 'active' => 'Is active?', 'percentage' => 'pct.', + 'recurring_transaction' => 'Recurring transaction', 'next_due' => 'Next due', 'transaction_type' => 'Type', 'lastActivity' => 'Last activity', diff --git a/resources/views/v1/transactions/show.twig b/resources/views/v1/transactions/show.twig index c485c08c62..1e0d26d00e 100644 --- a/resources/views/v1/transactions/show.twig +++ b/resources/views/v1/transactions/show.twig @@ -276,6 +276,16 @@ {{ journal.notes|markdown }} {% endif %} + {% if journalHasMeta(journal.transaction_journal_id, 'recurring_total') and journalHasMeta(journal.transaction_journal_id, 'recurring_count') %} + {% set recurringTotal = journalGetMetaField(journal.transaction_journal_id, 'recurring_total') %} + {% if 0 == recurringTotal %} + {% set recurringTotal = '∞' %} + {% endif %} + + {{ trans('list.recurring_transaction') }} + {{ trans('firefly.recurring_info', {total: recurringTotal, count: journalGetMetaField(journal.transaction_journal_id, 'recurring_count') }) }} + + {% endif %} {% if journal.tags|length > 0 %} {{ 'tags'|_ }}