This commit is contained in:
James Cole 2020-07-26 07:57:48 +02:00
parent 4b16d7c53d
commit 83467ef2f2
No known key found for this signature in database
GPG Key ID: B5669F9493CDE38D
10 changed files with 112 additions and 56 deletions

View File

@ -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')) {

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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.
*

View File

@ -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;
/**

View File

@ -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++;

View File

@ -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;
}
}

View File

@ -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.',

View File

@ -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',

View File

@ -276,6 +276,16 @@
<td class="markdown">{{ journal.notes|markdown }}</td>
</tr>
{% 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 %}
<tr>
<td>{{ 'tags'|_ }}</td>