mirror of
				https://github.com/firefly-iii/firefly-iii.git
				synced 2025-10-31 02:36:28 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			391 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			391 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| /**
 | |
|  * BillTransformer.php
 | |
|  * Copyright (c) 2019 james@firefly-iii.org
 | |
|  *
 | |
|  * This file is part of Firefly III (https://github.com/firefly-iii).
 | |
|  *
 | |
|  * This program is free software: you can redistribute it and/or modify
 | |
|  * it under the terms of the GNU Affero General Public License as
 | |
|  * published by the Free Software Foundation, either version 3 of the
 | |
|  * License, or (at your option) any later version.
 | |
|  *
 | |
|  * This program is distributed in the hope that it will be useful,
 | |
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | |
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | |
|  * GNU Affero General Public License for more details.
 | |
|  *
 | |
|  * You should have received a copy of the GNU Affero General Public License
 | |
|  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 | |
|  */
 | |
| 
 | |
| declare(strict_types=1);
 | |
| 
 | |
| namespace FireflyIII\Transformers\V2;
 | |
| 
 | |
| use Carbon\Carbon;
 | |
| use Carbon\CarbonInterface;
 | |
| use FireflyIII\Models\Bill;
 | |
| use FireflyIII\Models\Note;
 | |
| use FireflyIII\Models\ObjectGroup;
 | |
| use FireflyIII\Models\Transaction;
 | |
| use FireflyIII\Models\TransactionCurrency;
 | |
| use FireflyIII\Models\TransactionJournal;
 | |
| use FireflyIII\Support\Http\Api\ExchangeRateConverter;
 | |
| use Illuminate\Support\Collection;
 | |
| use Illuminate\Support\Facades\DB;
 | |
| 
 | |
| /**
 | |
|  * Class BillTransformer
 | |
|  */
 | |
| class BillTransformer extends AbstractTransformer
 | |
| {
 | |
|     private ExchangeRateConverter $converter;
 | |
|     private array                 $currencies;
 | |
|     private TransactionCurrency   $default;
 | |
|     private array                 $groups;
 | |
|     private array                 $notes;
 | |
|     private array                 $paidDates;
 | |
| 
 | |
|     /**
 | |
|      * @inheritDoc
 | |
|      */
 | |
|     public function collectMetaData(Collection $objects): void
 | |
|     {
 | |
|         $currencies      = [];
 | |
|         $bills           = [];
 | |
|         $this->notes     = [];
 | |
|         $this->groups    = [];
 | |
|         $this->paidDates = [];
 | |
| 
 | |
| 
 | |
|         // start with currencies:
 | |
|         /** @var Bill $object */
 | |
|         foreach ($objects as $object) {
 | |
|             $id              = $object->transaction_currency_id;
 | |
|             $bills[]         = $object->id;
 | |
|             $currencies[$id] ??= TransactionCurrency::find($id);
 | |
|         }
 | |
|         $this->currencies = $currencies;
 | |
| 
 | |
|         // continue with notes
 | |
|         $notes = Note::whereNoteableType(Bill::class)->whereIn('noteable_id', array_keys($bills))->get();
 | |
|         /** @var Note $note */
 | |
|         foreach ($notes as $note) {
 | |
|             $id               = $note->noteable_id;
 | |
|             $this->notes[$id] = $note;
 | |
|         }
 | |
|         // grab object groups:
 | |
|         $set = DB::table('object_groupables')
 | |
|                  ->leftJoin('object_groups', 'object_groups.id', '=', 'object_groupables.object_group_id')
 | |
|                  ->where('object_groupables.object_groupable_type', Bill::class)
 | |
|                  ->get(['object_groupables.*', 'object_groups.title', 'object_groups.order']);
 | |
|         /** @var ObjectGroup $entry */
 | |
|         foreach ($set as $entry) {
 | |
|             $billId                = (int)$entry->object_groupable_id;
 | |
|             $id                    = (int)$entry->object_group_id;
 | |
|             $order                 = $entry->order;
 | |
|             $this->groups[$billId] = [
 | |
|                 'object_group_id'    => $id,
 | |
|                 'object_group_title' => $entry->title,
 | |
|                 'object_group_order' => $order,
 | |
|             ];
 | |
| 
 | |
|         }
 | |
|         $this->default   = app('amount')->getDefaultCurrency();
 | |
|         $this->converter = new ExchangeRateConverter();
 | |
| 
 | |
|         // grab all paid dates:
 | |
|         if (null !== $this->parameters->get('start') && null !== $this->parameters->get('end')) {
 | |
|             $journals   = TransactionJournal::whereIn('bill_id', $bills)
 | |
|                                             ->where('date', '>=', $this->parameters->get('start'))
 | |
|                                             ->where('date', '<=', $this->parameters->get('end'))
 | |
|                                             ->get(['transaction_journals.id', 'transaction_journals.transaction_group_id', 'transaction_journals.date', 'transaction_journals.bill_id']);
 | |
|             $journalIds = $journals->pluck('id')->toArray();
 | |
| 
 | |
|             // grab transactions for amount:
 | |
|             $set = Transaction::whereIn('transaction_journal_id', $journalIds)
 | |
|                               ->where('transactions.amount', '<', 0)
 | |
|                               ->get(['transactions.id', 'transactions.transaction_journal_id', 'transactions.amount', 'transactions.foreign_amount', 'transactions.transaction_currency_id', 'transactions.foreign_currency_id']);
 | |
|             // convert to array for easy finding:
 | |
|             $transactions = [];
 | |
|             /** @var Transaction $transaction */
 | |
|             foreach ($set as $transaction) {
 | |
|                 $journalId                = $transaction->transaction_journal_id;
 | |
|                 $transactions[$journalId] = $transaction->toArray();
 | |
|             }
 | |
|             /** @var TransactionJournal $journal */
 | |
|             foreach ($journals as $journal) {
 | |
|                 app('log')->debug(sprintf('Processing journal #%d', $journal->id));
 | |
|                 $transaction             = $transactions[$journal->id] ?? [];
 | |
|                 $billId                  = (int)$journal->bill_id;
 | |
|                 $currencyId              = (int)($transaction['transaction_currency_id'] ?? 0);
 | |
|                 $currencies[$currencyId] ??= TransactionCurrency::find($currencyId);
 | |
| 
 | |
|                 // foreign currency
 | |
|                 $foreignCurrencyId     = null;
 | |
|                 $foreignCurrencyCode   = null;
 | |
|                 $foreignCurrencyName   = null;
 | |
|                 $foreignCurrencySymbol = null;
 | |
|                 $foreignCurrencyDp     = null;
 | |
|                 app('log')->debug('Foreign currency is NULL');
 | |
|                 if (null !== $transaction['foreign_currency_id']) {
 | |
|                     app('log')->debug(sprintf('Foreign currency is #%d', $transaction['foreign_currency_id']));
 | |
|                     $foreignCurrencyId              = (int)$transaction['foreign_currency_id'];
 | |
|                     $currencies[$foreignCurrencyId] ??= TransactionCurrency::find($foreignCurrencyId);
 | |
|                     $foreignCurrencyCode            = $currencies[$foreignCurrencyId]->code;
 | |
|                     $foreignCurrencyName            = $currencies[$foreignCurrencyId]->name;
 | |
|                     $foreignCurrencySymbol          = $currencies[$foreignCurrencyId]->symbol;
 | |
|                     $foreignCurrencyDp              = $currencies[$foreignCurrencyId]->decimal_places;
 | |
|                 }
 | |
| 
 | |
|                 $this->paidDates[$billId][] = [
 | |
|                     'transaction_group_id'            => (string)$journal->id,
 | |
|                     'transaction_journal_id'          => (string)$journal->transaction_group_id,
 | |
|                     'date'                            => $journal->date->toAtomString(),
 | |
|                     'currency_id'                     => $currencies[$currencyId]->id,
 | |
|                     'currency_code'                   => $currencies[$currencyId]->code,
 | |
|                     'currency_name'                   => $currencies[$currencyId]->name,
 | |
|                     'currency_symbol'                 => $currencies[$currencyId]->symbol,
 | |
|                     'currency_decimal_places'         => $currencies[$currencyId]->decimal_places,
 | |
|                     'native_currency_id'              => $currencies[$currencyId]->id,
 | |
|                     'native_currency_code'            => $currencies[$currencyId]->code,
 | |
|                     'native_currency_symbol'          => $currencies[$currencyId]->symbol,
 | |
|                     'native_currency_decimal_places'  => $currencies[$currencyId]->decimal_places,
 | |
|                     'foreign_currency_id'             => $foreignCurrencyId,
 | |
|                     'foreign_currency_code'           => $foreignCurrencyCode,
 | |
|                     'foreign_currency_name'           => $foreignCurrencyName,
 | |
|                     'foreign_currency_symbol'         => $foreignCurrencySymbol,
 | |
|                     'foreign_currency_decimal_places' => $foreignCurrencyDp,
 | |
|                     'amount'                          => $transaction['amount'],
 | |
|                     'foreign_amount'                  => $transaction['foreign_amount'],
 | |
|                     'native_amount'                   => $this->converter->convert($currencies[$currencyId], $this->default, $journal->date, $transaction['amount']),
 | |
|                     'foreign_native_amount'           => '' === (string)$transaction['foreign_amount'] ? null : $this->converter->convert(
 | |
|                         $currencies[$foreignCurrencyId],
 | |
|                         $this->default,
 | |
|                         $journal->date,
 | |
|                         $transaction['foreign_amount']
 | |
|                     ),
 | |
|                 ];
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Transform the bill.
 | |
|      *
 | |
|      * @param Bill $bill
 | |
|      *
 | |
|      * @return array
 | |
|      */
 | |
|     public function transform(Bill $bill): array
 | |
|     {
 | |
|         $paidData          = $this->paidDates[$bill->id] ?? [];
 | |
|         $nextExpectedMatch = $this->nextExpectedMatch($bill, $this->paidDates[$bill->id] ?? []);
 | |
|         $payDates          = $this->payDates($bill);
 | |
|         $currency          = $this->currencies[$bill->transaction_currency_id];
 | |
|         $group             = $this->groups[$bill->id] ?? null;
 | |
| 
 | |
|         // date for currency conversion
 | |
|         /** @var Carbon|null $startParam */
 | |
|         $startParam = $this->parameters->get('start');
 | |
|         /** @var Carbon|null $date */
 | |
|         $date = null === $startParam ? today() : clone $startParam;
 | |
| 
 | |
| 
 | |
|         $nextExpectedMatchDiff = $this->getNextExpectedMatchDiff($nextExpectedMatch, $payDates);
 | |
|         return [
 | |
|             'id'                             => $bill->id,
 | |
|             'created_at'                     => $bill->created_at->toAtomString(),
 | |
|             'updated_at'                     => $bill->updated_at->toAtomString(),
 | |
|             'name'                           => $bill->name,
 | |
|             'amount_min'                     => app('steam')->bcround($bill->amount_min, $currency->decimal_places),
 | |
|             'amount_max'                     => app('steam')->bcround($bill->amount_max, $currency->decimal_places),
 | |
|             'native_amount_min'              => $this->converter->convert($currency, $this->default, $date, $bill->amount_min),
 | |
|             'native_amount_max'              => $this->converter->convert($currency, $this->default, $date, $bill->amount_max),
 | |
|             'currency_id'                    => (string)$bill->transaction_currency_id,
 | |
|             'currency_code'                  => $currency->code,
 | |
|             'currency_name'                  => $currency->name,
 | |
|             'currency_symbol'                => $currency->symbol,
 | |
|             'currency_decimal_places'        => $currency->decimal_places,
 | |
|             'native_currency_id'             => $this->default->id,
 | |
|             'native_currency_code'           => $this->default->code,
 | |
|             'native_currency_name'           => $this->default->name,
 | |
|             'native_currency_symbol'         => $this->default->symbol,
 | |
|             'native_currency_decimal_places' => $this->default->decimal_places,
 | |
|             'date'                           => $bill->date->toAtomString(),
 | |
|             'end_date'                       => $bill->end_date?->toAtomString(),
 | |
|             'extension_date'                 => $bill->extension_date?->toAtomString(),
 | |
|             'repeat_freq'                    => $bill->repeat_freq,
 | |
|             'skip'                           => $bill->skip,
 | |
|             'active'                         => $bill->active,
 | |
|             'order'                          => $bill->order,
 | |
|             'notes'                          => $this->notes[$bill->id] ?? null,
 | |
|             'object_group_id'                => $group ? $group['object_group_id'] : null,
 | |
|             'object_group_order'             => $group ? $group['object_group_order'] : null,
 | |
|             'object_group_title'             => $group ? $group['object_group_title'] : null,
 | |
|             'next_expected_match'            => $nextExpectedMatch->toAtomString(),
 | |
|             'next_expected_match_diff'       => $nextExpectedMatchDiff,
 | |
|             'pay_dates'                      => $payDates,
 | |
|             'paid_dates'                     => $paidData,
 | |
|             'links'                          => [
 | |
|                 [
 | |
|                     'rel' => 'self',
 | |
|                     'uri' => sprintf('/bills/%d', $bill->id),
 | |
|                 ],
 | |
|             ],
 | |
|         ];
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Get the data the bill was paid and predict the next expected match.
 | |
|      *
 | |
|      * @param Bill  $bill
 | |
|      * @param array $dates
 | |
|      *
 | |
|      * @return Carbon
 | |
|      */
 | |
|     protected function nextExpectedMatch(Bill $bill, array $dates): Carbon
 | |
|     {
 | |
|         // 2023-07-1 sub one day from the start date to fix a possible bug (see #7704)
 | |
|         // 2023-07-18 this particular date is used to search for the last paid date.
 | |
|         // 2023-07-18 the cloned $searchDate is used to grab the correct transactions.
 | |
| 
 | |
|         /** @var Carbon|null $startParam */
 | |
|         $startParam = $this->parameters->get('start');
 | |
|         /** @var Carbon|null $start */
 | |
|         $start = null === $startParam ? today() : clone $startParam;
 | |
|         $start->subDay();
 | |
| 
 | |
|         $lastPaidDate = $this->lastPaidDate($dates, $start);
 | |
|         $nextMatch    = clone $bill->date;
 | |
|         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.
 | |
|              */
 | |
|             $nextMatch = app('navigation')->addPeriod($nextMatch, $bill->repeat_freq, $bill->skip);
 | |
|         }
 | |
|         if ($nextMatch->isSameDay($lastPaidDate)) {
 | |
|             /*
 | |
|              * Add another period because it's the same day as the last paid date.
 | |
|              */
 | |
|             $nextMatch = app('navigation')->addPeriod($nextMatch, $bill->repeat_freq, $bill->skip);
 | |
|         }
 | |
|         return $nextMatch;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Returns the latest date in the set, or start when set is empty.
 | |
|      *
 | |
|      * @param array  $dates
 | |
|      * @param Carbon $default
 | |
|      *
 | |
|      * @return Carbon
 | |
|      */
 | |
|     protected function lastPaidDate(array $dates, Carbon $default): Carbon
 | |
|     {
 | |
|         if (0 === count($dates)) {
 | |
|             return $default;
 | |
|         }
 | |
|         $latest = $dates[0]['date'];
 | |
|         /** @var array $row */
 | |
|         foreach ($dates as $row) {
 | |
|             $carbon = new Carbon($row['date']);
 | |
|             if ($carbon->gte($latest)) {
 | |
|                 $latest = $row['date'];
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return new Carbon($latest);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * @param Bill $bill
 | |
|      *
 | |
|      * @return array
 | |
|      */
 | |
|     protected function payDates(Bill $bill): array
 | |
|     {
 | |
|         //app('log')->debug(sprintf('Now in payDates() for bill #%d', $bill->id));
 | |
|         if (null === $this->parameters->get('start') || null === $this->parameters->get('end')) {
 | |
|             //app('log')->debug('No start or end date, give empty array.');
 | |
| 
 | |
|             return [];
 | |
|         }
 | |
|         $set          = new Collection();
 | |
|         $currentStart = clone $this->parameters->get('start');
 | |
|         // 2023-06-23 subDay to fix 7655
 | |
|         $currentStart->subDay();
 | |
|         $loop = 0;
 | |
|         while ($currentStart <= $this->parameters->get('end')) {
 | |
|             $nextExpectedMatch = $this->nextDateMatch($bill, $currentStart);
 | |
|             // If nextExpectedMatch is after end, we continue:
 | |
|             if ($nextExpectedMatch > $this->parameters->get('end')) {
 | |
|                 break;
 | |
|             }
 | |
|             // add to set
 | |
|             $set->push(clone $nextExpectedMatch);
 | |
|             $nextExpectedMatch->addDay();
 | |
|             $currentStart = clone $nextExpectedMatch;
 | |
|             $loop++;
 | |
|             if ($loop > 4) {
 | |
|                 break;
 | |
|             }
 | |
|         }
 | |
|         $simple = $set->map(
 | |
|             static function (Carbon $date) {
 | |
|                 return $date->toAtomString();
 | |
|             }
 | |
|         );
 | |
| 
 | |
|         return $simple->toArray();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 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.
 | |
|      *
 | |
|      * TODO this method is bad compared to the v1 one.
 | |
|      *
 | |
|      * @param Bill   $bill
 | |
|      * @param Carbon $date
 | |
|      *
 | |
|      * @return Carbon
 | |
|      */
 | |
|     protected function nextDateMatch(Bill $bill, Carbon $date): Carbon
 | |
|     {
 | |
|         //app('log')->debug(sprintf('Now in nextDateMatch(%d, %s)', $bill->id, $date->format('Y-m-d')));
 | |
|         $start = clone $bill->date;
 | |
|         //app('log')->debug(sprintf('Bill start date is %s', $start->format('Y-m-d')));
 | |
|         while ($start < $date) {
 | |
|             $start = app('navigation')->addPeriod($start, $bill->repeat_freq, $bill->skip);
 | |
|         }
 | |
| 
 | |
|         //app('log')->debug(sprintf('End of loop, bill start date is now %s', $start->format('Y-m-d')));
 | |
| 
 | |
|         return $start;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * @param Carbon $next
 | |
|      * @param array  $dates
 | |
|      *
 | |
|      * @return string
 | |
|      */
 | |
|     private function getNextExpectedMatchDiff(Carbon $next, array $dates): string
 | |
|     {
 | |
|         if ($next->isToday()) {
 | |
|             return trans('firefly.today');
 | |
|         }
 | |
|         $current = $dates[0] ?? null;
 | |
|         if (null === $current) {
 | |
|             return trans('firefly.not_expected_period');
 | |
|         }
 | |
|         $carbon = new Carbon($current);
 | |
|         return $carbon->diffForHumans(today(config('app.timezone')), CarbonInterface::DIFF_RELATIVE_TO_NOW);
 | |
|     }
 | |
| 
 | |
| 
 | |
| }
 |