. */ declare(strict_types=1); namespace FireflyIII\Transformers; use Carbon\Carbon; use FireflyIII\Models\Bill; use FireflyIII\Models\TransactionJournal; use FireflyIII\Repositories\Bill\BillRepositoryInterface; use Illuminate\Support\Collection; use Log; /** * Class BillTransformer */ class BillTransformer extends AbstractTransformer { /** @var BillRepositoryInterface */ private $repository; /** * BillTransformer constructor. * * @codeCoverageIgnore */ public function __construct() { $this->repository = app(BillRepositoryInterface::class); if ('testing' === config('app.env')) { Log::warning(sprintf('%s should not be instantiated in the TEST environment!', get_class($this))); } } /** * Transform the bill. * * @param Bill $bill * * @return array */ public function transform(Bill $bill): array { $paidData = $this->paidData($bill); $payDates = $this->payDates($bill); $currency = $bill->transactionCurrency; $notes = $this->repository->getNoteText($bill); $notes = '' === $notes ? null : $notes; $this->repository->setUser($bill->user); $data = [ 'id' => (int)$bill->id, 'created_at' => $bill->created_at->toAtomString(), 'updated_at' => $bill->updated_at->toAtomString(), 'currency_id' => $bill->transaction_currency_id, 'currency_code' => $currency->code, 'currency_symbol' => $currency->symbol, 'currency_decimal_places' => $currency->decimal_places, 'name' => $bill->name, 'amount_min' => round((float)$bill->amount_min, $currency->decimal_places), 'amount_max' => round((float)$bill->amount_max, $currency->decimal_places), 'date' => $bill->date->format('Y-m-d'), 'repeat_freq' => $bill->repeat_freq, 'skip' => (int)$bill->skip, 'active' => $bill->active, 'notes' => $notes, 'next_expected_match' => $paidData['next_expected_match'], 'pay_dates' => $payDates, 'paid_dates' => $paidData['paid_dates'], 'links' => [ [ 'rel' => 'self', 'uri' => '/bills/' . $bill->id, ], ], ]; return $data; } /** * Returns the latest date in the set, or start when set is empty. * * @param Collection $dates * @param Carbon $default * * @return Carbon */ protected function lastPaidDate(Collection $dates, Carbon $default): Carbon { if (0 === $dates->count()) { return $default; // @codeCoverageIgnore } $latest = $dates->first()->date; /** @var TransactionJournal $date */ foreach ($dates as $journal) { if ($journal->date->gte($latest)) { $latest = $journal->date; } } return $latest; } /** * Given a bill and a date, this method will tell you at which moment this bill expects its next * transaction. Whether or not it is there already, is not relevant. * * @param Bill $bill * @param Carbon $date * * @return Carbon */ protected function nextDateMatch(Bill $bill, Carbon $date): Carbon { Log::debug(sprintf('Now in nextDateMatch(%d, %s)', $bill->id, $date->format('Y-m-d'))); $start = clone $bill->date; Log::debug(sprintf('Bill start date is %s', $start->format('Y-m-d'))); while ($start < $date) { Log::debug( sprintf( '%s (bill start date) < %s (given date) so we jump ahead one period (with a skip maybe).', $start->format('Y-m-d'), $date->format('Y-m-d') ) ); $start = app('navigation')->addPeriod($start, $bill->repeat_freq, $bill->skip); } Log::debug(sprintf('End of loop, bill start date is now %s', $start->format('Y-m-d'))); return $start; } /** * Get the data the bill was paid and predict the next expected match. * * @param Bill $bill * * @return array */ protected function paidData(Bill $bill): array { Log::debug(sprintf('Now in paidData for bill #%d', $bill->id)); if (null === $this->parameters->get('start') || null === $this->parameters->get('end')) { Log::debug('parameters are NULL, return empty array'); return [ 'paid_dates' => [], 'next_expected_match' => null, ]; } Log::debug(sprintf('Parameters are start:%s end:%s', $this->parameters->get('start')->format('Y-m-d'), $this->parameters->get('end')->format('Y-m-d'))); /* * Get from database when bill was paid. */ $set = $this->repository->getPaidDatesInRange($bill, $this->parameters->get('start'), $this->parameters->get('end')); Log::debug(sprintf('Count %d entries in getPaidDatesInRange()', $set->count())); /* * Grab from array the most recent payment. If none exist, fall back to the start date and pretend *that* was the last paid date. */ Log::debug(sprintf('Grab last paid date from function, return %s if it comes up with nothing.', $this->parameters->get('start')->format('Y-m-d'))); $lastPaidDate = $this->lastPaidDate($set, $this->parameters->get('start')); Log::debug(sprintf('Result of lastPaidDate is %s', $lastPaidDate->format('Y-m-d'))); /* * The next expected match (nextMatch) is, initially, the bill's date. */ $nextMatch = clone $bill->date; Log::debug(sprintf('Next match is %s (bill->date)', $nextMatch->format('Y-m-d'))); while ($nextMatch < $lastPaidDate) { /* * As long as this date is smaller than the last time the bill was paid, keep jumping ahead. * For example: 1 jan, 1 feb, etc. */ Log::debug(sprintf('next match %s < last paid date %s, so add one period.', $nextMatch->format('Y-m-d'), $lastPaidDate->format('Y-m-d'))); $nextMatch = app('navigation')->addPeriod($nextMatch, $bill->repeat_freq, $bill->skip); Log::debug(sprintf('Next match is now %s.', $nextMatch->format('Y-m-d'))); } if($nextMatch->isSameDay($lastPaidDate)) { /* * Add another period because its the same day as the last paid date. */ Log::debug('Because the last paid date was on the same day as our next expected match, add another day.'); $nextMatch = app('navigation')->addPeriod($nextMatch, $bill->repeat_freq, $bill->skip); } /* * At this point the "next match" is exactly after the last time the bill was paid. */ $result = []; foreach ($set as $entry) { $result[] = [ 'transaction_group_id' => (int)$entry->transaction_group_id, 'transaction_journal_id' => (int)$entry->id, 'date' => $entry->date->format('Y-m-d'), ]; } $result = [ 'paid_dates' => $result, 'next_expected_match' => $nextMatch->format('Y-m-d'), ]; Log::debug('Result', $result); return $result; } /** * @param Bill $bill * * @return array */ protected function payDates(Bill $bill): array { Log::debug(sprintf('Now in payDates() for bill #%d', $bill->id)); if (null === $this->parameters->get('start') || null === $this->parameters->get('end')) { Log::debug('No start or end date, give empty array.'); return []; } Log::debug( sprintf( 'Start date is %s, end is %s', $this->parameters->get('start')->format('Y-m-d'), $this->parameters->get('end')->format('Y-m-d') ) ); $set = new Collection; $currentStart = clone $this->parameters->get('start'); $loop = 0; while ($currentStart <= $this->parameters->get('end')) { Log::debug( sprintf( 'In loop #%d, where %s (start param) <= %s (end param).', $loop, $currentStart->format('Y-m-d'), $this->parameters->get('end')->format('Y-m-d') ) ); $nextExpectedMatch = $this->nextDateMatch($bill, $currentStart); Log::debug(sprintf('Next expected match is %s', $nextExpectedMatch->format('Y-m-d'))); // If nextExpectedMatch is after end, we continue: if ($nextExpectedMatch > $this->parameters->get('end')) { Log::debug( sprintf('%s is > %s, so were not going to use it.', $nextExpectedMatch->format('Y-m-d'), $this->parameters->get('end')->format('Y-m-d')) ); break; } // add to set $set->push(clone $nextExpectedMatch); Log::debug(sprintf('Add next expected match (%s) to set because its in the current start/end range, which now contains %d item(s)', $nextExpectedMatch->format('Y-m-d'), $set->count())); $nextExpectedMatch->addDay(); $currentStart = clone $nextExpectedMatch; $loop++; } $simple = $set->map( static function (Carbon $date) { return $date->format('Y-m-d'); } ); $array = $simple->toArray(); Log::debug(sprintf('Loop has ended after %d loops', $loop), $array); return $array; } }