This commit is contained in:
James Cole
2020-07-26 07:57:48 +02:00
parent 4b16d7c53d
commit 83467ef2f2
10 changed files with 112 additions and 56 deletions

View File

@@ -56,12 +56,9 @@ class TransactionJournalFactory
{ {
use JournalServiceTrait; use JournalServiceTrait;
/** @var AccountRepositoryInterface */ private AccountRepositoryInterface $accountRepository;
private $accountRepository; private AccountValidator $accountValidator;
/** @var AccountValidator */ private BillRepositoryInterface $billRepository;
private $accountValidator;
/** @var BillRepositoryInterface */
private $billRepository;
/** @var CurrencyRepositoryInterface */ /** @var CurrencyRepositoryInterface */
private $currencyRepository; private $currencyRepository;
/** @var bool */ /** @var bool */
@@ -88,6 +85,7 @@ class TransactionJournalFactory
public function __construct() public function __construct()
{ {
$this->errorOnHash = false; $this->errorOnHash = false;
// TODO move valid meta fields to config.
$this->fields = [ $this->fields = [
// sepa // sepa
'sepa_cc', 'sepa_ct_op', 'sepa_ct_id', 'sepa_cc', 'sepa_ct_op', 'sepa_ct_id',
@@ -100,7 +98,11 @@ class TransactionJournalFactory
// others // others
'recurrence_id', 'internal_reference', 'bunq_payment_id', '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')) { if ('testing' === config('app.env')) {

View File

@@ -53,15 +53,15 @@ class CreateRecurringTransactions implements ShouldQueue
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/** @var int Transaction groups created */ /** @var int Transaction groups created */
public $created; public int $created;
/** @var int Number of recurrences actually fired */ /** @var int Number of recurrences actually fired */
public $executed; public int $executed;
/** @var int Number of recurrences submitted */ /** @var int Number of recurrences submitted */
public $submitted; public int $submitted;
/** @var Carbon The current date */ /** @var Carbon The current date */
private $date; private Carbon $date;
/** @var bool Force the transaction to be created no matter what. */ /** @var bool Force the transaction to be created no matter what. */
private $force; private bool $force;
/** @var TransactionGroupRepositoryInterface */ /** @var TransactionGroupRepositoryInterface */
private $groupRepository; private $groupRepository;
/** @var JournalRepositoryInterface Journal repository */ /** @var JournalRepositoryInterface Journal repository */
@@ -219,19 +219,23 @@ class CreateRecurringTransactions implements ShouldQueue
/** /**
* Get transaction information from a recurring transaction. * Get transaction information from a recurring transaction.
* *
* @param Recurrence $recurrence * @param Recurrence $recurrence
* @param Carbon $date * @param RecurrenceRepetition $repetition
* @param Carbon $date
* *
* @return array * @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(); $transactions = $recurrence->recurrenceTransactions()->get();
$return = []; $return = [];
/** @var RecurrenceTransaction $transaction */ /** @var RecurrenceTransaction $transaction */
foreach ($transactions as $index => $transaction) { foreach ($transactions as $index => $transaction) {
$single = [ $single = [
'type' => strtolower($recurrence->transactionType->type), 'type' => strtolower($recurrence->transactionType->type),
'date' => $date, 'date' => $date,
'user' => $recurrence->user_id, 'user' => $recurrence->user_id,
@@ -260,6 +264,8 @@ class CreateRecurringTransactions implements ShouldQueue
'piggy_bank_name' => null, 'piggy_bank_name' => null,
'bill_id' => null, 'bill_id' => null,
'bill_name' => null, 'bill_name' => null,
'recurrence_total' => $total,
'recurrence_count' => $count,
]; ];
$return[] = $single; $return[] = $single;
} }
@@ -268,17 +274,18 @@ class CreateRecurringTransactions implements ShouldQueue
} }
/** /**
* @param Recurrence $recurrence * @param Recurrence $recurrence
* @param Carbon $date * @param RecurrenceRepetition $repetition
* @param Carbon $date
* *
* @return TransactionGroup|null * @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(); $date->startOfDay();
if ($date->ne($this->date)) { 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; return null;
} }
@@ -305,10 +312,11 @@ class CreateRecurringTransactions implements ShouldQueue
$groupTitle = $first->description; $groupTitle = $first->description;
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd
} }
$array = [ $array = [
'user' => $recurrence->user_id, 'user' => $recurrence->user_id,
'group_title' => $groupTitle, 'group_title' => $groupTitle,
'transactions' => $this->getTransactionData($recurrence, $date), 'transactions' => $this->getTransactionData($recurrence, $repetition, $date),
]; ];
/** @var TransactionGroup $group */ /** @var TransactionGroup $group */
$group = $this->groupRepository->store($array); $group = $this->groupRepository->store($array);
@@ -328,17 +336,18 @@ class CreateRecurringTransactions implements ShouldQueue
/** /**
* Check if the occurences should be executed. * Check if the occurences should be executed.
* *
* @param Recurrence $recurrence * @param Recurrence $recurrence
* @param array $occurrences * @param RecurrenceRepetition $repetition
* @param array $occurrences
* *
* @return Collection * @return Collection
*/ */
private function handleOccurrences(Recurrence $recurrence, array $occurrences): Collection private function handleOccurrences(Recurrence $recurrence, RecurrenceRepetition $repetition, array $occurrences): Collection
{ {
$collection = new Collection; $collection = new Collection;
/** @var Carbon $date */ /** @var Carbon $date */
foreach ($occurrences as $date) { foreach ($occurrences as $date) {
$result = $this->handleOccurrence($recurrence, $date); $result = $this->handleOccurrence($recurrence, $repetition, $date);
if (null !== $result) { if (null !== $result) {
$collection->push($result); $collection->push($result);
} }
@@ -374,6 +383,7 @@ class CreateRecurringTransactions implements ShouldQueue
$includeWeekend = clone $this->date; $includeWeekend = clone $this->date;
$includeWeekend->addDays(2); $includeWeekend->addDays(2);
$occurrences = $this->repository->getOccurrencesInRange($repetition, $recurrence->first_date, $includeWeekend); $occurrences = $this->repository->getOccurrencesInRange($repetition, $recurrence->first_date, $includeWeekend);
/*
Log::debug( Log::debug(
sprintf( sprintf(
'Calculated %d occurrences between %s and %s', 'Calculated %d occurrences between %s and %s',
@@ -383,9 +393,10 @@ class CreateRecurringTransactions implements ShouldQueue
), ),
$this->debugArray($occurrences) $this->debugArray($occurrences)
); );
*/
unset($includeWeekend); unset($includeWeekend);
$result = $this->handleOccurrences($recurrence, $occurrences); $result = $this->handleOccurrences($recurrence, $repetition, $occurrences);
$collection = $collection->merge($result); $collection = $collection->merge($result);
} }

View File

@@ -569,4 +569,28 @@ class RecurringRepository implements RecurringRepositoryInterface
{ {
$this->user->recurrences()->delete(); $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;
}
} }

View File

@@ -44,6 +44,15 @@ interface RecurringRepositoryInterface
*/ */
public function destroyAll(): void; 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. * Destroy a recurring transaction.
* *

View File

@@ -44,14 +44,10 @@ use Log;
*/ */
trait JournalServiceTrait trait JournalServiceTrait
{ {
/** @var AccountRepositoryInterface */ private AccountRepositoryInterface $accountRepository;
private $accountRepository; private BudgetRepositoryInterface $budgetRepository;
/** @var BudgetRepositoryInterface */ private CategoryRepositoryInterface $categoryRepository;
private $budgetRepository; private TagFactory $tagFactory;
/** @var CategoryRepositoryInterface */
private $categoryRepository;
/** @var TagFactory */
private $tagFactory;
/** /**

View File

@@ -46,11 +46,11 @@ trait CalculateRangeOccurrences
{ {
$return = []; $return = [];
$attempts = 0; $attempts = 0;
Log::debug('Rep is daily. Start of loop.'); #Log::debug('Rep is daily. Start of loop.');
while ($start <= $end) { 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) { 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; $return[] = clone $start;
} }
$start->addDay(); $start->addDay();
@@ -77,27 +77,27 @@ trait CalculateRangeOccurrences
$return = []; $return = [];
$attempts = 0; $attempts = 0;
$dayOfMonth = (int)$moment; $dayOfMonth = (int)$moment;
Log::debug(sprintf('Day of month in repetition is %d', $dayOfMonth)); #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('Start is %s.', $start->format('Y-m-d')));
Log::debug(sprintf('End is %s.', $end->format('Y-m-d'))); #Log::debug(sprintf('End is %s.', $end->format('Y-m-d')));
if ($start->day > $dayOfMonth) { if ($start->day > $dayOfMonth) {
Log::debug('Add a month.'); #Log::debug('Add a month.');
// day has passed already, add a month. // day has passed already, add a month.
$start->addMonth(); $start->addMonth();
} }
Log::debug(sprintf('Start is now %s.', $start->format('Y-m-d'))); #Log::debug(sprintf('Start is now %s.', $start->format('Y-m-d')));
Log::debug('Start loop.'); #Log::debug('Start loop.');
while ($start < $end) { 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); $domCorrected = min($dayOfMonth, $start->daysInMonth);
Log::debug(sprintf('DoM corrected is %d', $domCorrected)); #Log::debug(sprintf('DoM corrected is %d', $domCorrected));
$start->day = $domCorrected; $start->day = $domCorrected;
Log::debug(sprintf('Mutator is now %s.', $start->format('Y-m-d'))); #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('$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('$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('$end->gte($mutator) is %s', var_export($end->gte($start), true)));
if (0 === $attempts % $skipMod && $start->lte($start) && $end->gte($start)) { 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; $return[] = clone $start;
} }
$attempts++; $attempts++;

View File

@@ -60,7 +60,8 @@ class TransactionGroupTransformer extends AbstractTransformer
$this->metaFields = [ $this->metaFields = [
'sepa_cc', 'sepa_ct_op', 'sepa_ct_id', 'sepa_db', 'sepa_country', 'sepa_ep', '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', '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']; $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'], 'bill_name' => $row['bill_name'],
'reconciled' => $row['reconciled'], 'reconciled' => $row['reconciled'],
'notes' => $this->groupRepos->getNoteText((int)$row['transaction_journal_id']), 'notes' => $this->groupRepos->getNoteText((int) $row['transaction_journal_id']),
'tags' => $this->groupRepos->getTags((int)$row['transaction_journal_id']), 'tags' => $this->groupRepos->getTags((int) $row['transaction_journal_id']),
'internal_reference' => $metaFieldData['internal_reference'], 'internal_reference' => $metaFieldData['internal_reference'],
'external_id' => $metaFieldData['external_id'], 'external_id' => $metaFieldData['external_id'],
'original_source' => $metaFieldData['original_source'], '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'], 'bunq_payment_id' => $metaFieldData['bunq_payment_id'],
'external_uri' => $metaFieldData['external_uri'], 'external_uri' => $metaFieldData['external_uri'],
'import_hash_v2' => $metaFieldData['import_hash_v2'], '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, 'invoice_date' => $metaDateData['invoice_date'] ? $metaDateData['invoice_date']->toAtomString() : null,
]; ];
} }
return $result; return $result;
} }
} }

View File

@@ -1637,6 +1637,7 @@ return [
'created_withdrawals' => 'Created withdrawals', 'created_withdrawals' => 'Created withdrawals',
'created_deposits' => 'Created deposits', 'created_deposits' => 'Created deposits',
'created_transfers' => 'Created transfers', 'created_transfers' => 'Created transfers',
'recurring_info' => 'Recurring transaction :count / :total',
'created_from_recurrence' => 'Created from recurring transaction ":title" (#:id)', '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_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.', '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.',

View File

@@ -37,6 +37,7 @@ return [
'linked_to_rules' => 'Relevant rules', 'linked_to_rules' => 'Relevant rules',
'active' => 'Is active?', 'active' => 'Is active?',
'percentage' => 'pct.', 'percentage' => 'pct.',
'recurring_transaction' => 'Recurring transaction',
'next_due' => 'Next due', 'next_due' => 'Next due',
'transaction_type' => 'Type', 'transaction_type' => 'Type',
'lastActivity' => 'Last activity', 'lastActivity' => 'Last activity',

View File

@@ -276,6 +276,16 @@
<td class="markdown">{{ journal.notes|markdown }}</td> <td class="markdown">{{ journal.notes|markdown }}</td>
</tr> </tr>
{% endif %} {% 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 %}
<tr>
<td>{{ trans('list.recurring_transaction') }}</td>
<td>{{ trans('firefly.recurring_info', {total: recurringTotal, count: journalGetMetaField(journal.transaction_journal_id, 'recurring_count') }) }}</td>
</tr>
{% endif %}
{% if journal.tags|length > 0 %} {% if journal.tags|length > 0 %}
<tr> <tr>
<td>{{ 'tags'|_ }}</td> <td>{{ 'tags'|_ }}</td>