2019-03-17 11:05:16 -05:00
< ? php
/**
* MigrateToGroups . php
* Copyright ( c ) 2019 thegrumpydictator @ gmail . com
*
* This file is part of Firefly III .
*
* Firefly III is free software : you can redistribute it and / or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation , either version 3 of the License , or
* ( at your option ) any later version .
*
* Firefly III 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 General Public License for more details .
*
* You should have received a copy of the GNU General Public License
* along with Firefly III . If not , see < http :// www . gnu . org / licenses />.
*/
namespace FireflyIII\Console\Commands\Upgrade ;
2019-03-23 12:58:06 -05:00
use DB ;
2019-03-17 11:05:16 -05:00
use Exception ;
2019-04-06 01:10:50 -05:00
use FireflyIII\Factory\TransactionGroupFactory ;
2019-03-17 11:05:16 -05:00
use FireflyIII\Models\Transaction ;
use FireflyIII\Models\TransactionJournal ;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface ;
use FireflyIII\Services\Internal\Destroy\JournalDestroyService ;
use Illuminate\Console\Command ;
2019-03-23 12:58:06 -05:00
use Illuminate\Support\Collection ;
2019-03-17 11:05:16 -05:00
use Log ;
/**
* This command will take split transactions and migrate them to " transaction groups " .
*
* It will only run once , but can be forced to run again .
*
* Class MigrateToGroups
*/
class MigrateToGroups extends Command
{
2019-03-18 10:53:05 -05:00
public const CONFIG_NAME = '4780_migrated_to_groups' ;
2019-03-17 11:05:16 -05:00
/**
* The console command description .
*
* @ var string
*/
protected $description = 'Migrates a pre-4.7.8 transaction structure to the 4.7.8+ transaction structure.' ;
/**
* The name and signature of the console command .
*
* @ var string
*/
2019-03-18 10:53:05 -05:00
protected $signature = 'firefly-iii:migrate-to-groups {--F|force : Force the migration, even if it fired before.}' ;
2019-04-06 01:10:50 -05:00
/** @var TransactionGroupFactory */
private $groupFactory ;
2019-03-17 11:05:16 -05:00
/** @var JournalRepositoryInterface */
private $journalRepository ;
2019-03-18 10:53:05 -05:00
/** @var JournalDestroyService */
private $service ;
2019-03-17 11:05:16 -05:00
/**
* Create a new command instance .
*
* @ return void
*/
public function __construct ()
{
parent :: __construct ();
$this -> journalRepository = app ( JournalRepositoryInterface :: class );
2019-03-18 10:53:05 -05:00
$this -> service = app ( JournalDestroyService :: class );
2019-04-06 01:10:50 -05:00
$this -> groupFactory = app ( TransactionGroupFactory :: class );
2019-03-17 11:05:16 -05:00
}
/**
* Execute the console command .
*
* @ return int
* @ throws Exception
*/
public function handle () : int
{
2019-03-23 12:58:06 -05:00
$start = microtime ( true );
2019-03-17 11:05:16 -05:00
if ( $this -> isMigrated () && true !== $this -> option ( 'force' )) {
$this -> info ( 'Database already seems to be migrated.' );
2019-03-18 10:53:05 -05:00
return 0 ;
2019-03-17 11:05:16 -05:00
}
if ( true === $this -> option ( 'force' )) {
$this -> warn ( 'Forcing the migration.' );
}
Log :: debug ( '---- start group migration ----' );
2019-03-23 02:10:59 -05:00
$this -> makeGroupsFromSplitJournals ();
2019-03-23 12:58:06 -05:00
$end = round ( microtime ( true ) - $start , 2 );
$this -> info ( sprintf ( 'Migrate split journals to groups in %s seconds.' , $end ));
$start = microtime ( true );
2019-03-23 02:10:59 -05:00
$this -> makeGroupsFromAll ();
2019-03-17 11:05:16 -05:00
Log :: debug ( '---- end group migration ----' );
2019-03-23 12:58:06 -05:00
$end = round ( microtime ( true ) - $start , 2 );
$this -> info ( sprintf ( 'Migrate all journals to groups in %s seconds.' , $end ));
2019-03-17 11:05:16 -05:00
$this -> markAsMigrated ();
return 0 ;
}
2019-03-23 02:10:59 -05:00
/**
* @ param TransactionJournal $journal
2019-03-23 12:58:06 -05:00
* @ param Transaction $transaction
*
* @ return Transaction | null
*/
private function findOpposingTransaction ( TransactionJournal $journal , Transaction $transaction ) : ? Transaction
{
$set = $journal -> transactions -> filter (
2019-04-06 01:10:50 -05:00
static function ( Transaction $subject ) use ( $transaction ) {
2019-03-23 12:58:06 -05:00
return $transaction -> amount * - 1 === ( float ) $subject -> amount && $transaction -> identifier === $subject -> identifier ;
}
);
return $set -> first ();
}
/**
* @ param TransactionJournal $journal
*
* @ return Collection
2019-03-23 02:10:59 -05:00
*/
2019-03-23 12:58:06 -05:00
private function getDestinationTransactions ( TransactionJournal $journal ) : Collection
2019-03-23 02:10:59 -05:00
{
2019-03-23 12:58:06 -05:00
return $journal -> transactions -> filter (
2019-04-06 01:10:50 -05:00
static function ( Transaction $transaction ) {
2019-03-23 12:58:06 -05:00
return $transaction -> amount > 0 ;
}
);
}
/**
* @ param array $array
*/
private function giveGroup ( array $array ) : void
{
2019-03-24 03:23:10 -05:00
$groupId = DB :: table ( 'transaction_groups' ) -> insertGetId (
[
'created_at' => date ( 'Y-m-d H:i:s' ),
'updated_at' => date ( 'Y-m-d H:i:s' ),
'title' => null ,
'user_id' => $array [ 'user_id' ],
]
);
2019-03-23 12:58:06 -05:00
DB :: table ( 'transaction_journals' ) -> where ( 'id' , $array [ 'id' ]) -> update ([ 'transaction_group_id' => $groupId ]);
2019-03-23 02:10:59 -05:00
}
2019-03-17 11:05:16 -05:00
/**
* @ return bool
*/
private function isMigrated () : bool
{
2019-03-18 10:53:05 -05:00
$configVar = app ( 'fireflyconfig' ) -> get ( self :: CONFIG_NAME , false );
2019-03-17 11:05:16 -05:00
if ( null !== $configVar ) {
return ( bool ) $configVar -> data ;
}
2019-03-18 10:53:05 -05:00
return false ; // @codeCoverageIgnore
2019-03-17 11:05:16 -05:00
}
2019-03-23 02:10:59 -05:00
/**
* Gives all journals without a group a group .
*/
private function makeGroupsFromAll () : void
{
$orphanedJournals = $this -> journalRepository -> getJournalsWithoutGroup ();
2019-03-23 12:58:06 -05:00
$count = count ( $orphanedJournals );
if ( $count > 0 ) {
Log :: debug ( sprintf ( 'Going to convert %d transaction journals. Please hold..' , $count ));
$this -> line ( sprintf ( 'Going to convert %d transaction journals. Please hold..' , $count ));
/** @var array $journal */
foreach ( $orphanedJournals as $array ) {
$this -> giveGroup ( $array );
2019-03-23 02:10:59 -05:00
}
}
2019-03-23 12:58:06 -05:00
if ( 0 === $count ) {
$this -> info ( 'No need to convert transaction journals.' );
2019-03-23 02:10:59 -05:00
}
}
/**
* @ throws Exception
*/
private function makeGroupsFromSplitJournals () : void
{
$splitJournals = $this -> journalRepository -> getSplitJournals ();
if ( $splitJournals -> count () > 0 ) {
$this -> info ( sprintf ( 'Going to convert %d split transaction(s). Please hold..' , $splitJournals -> count ()));
/** @var TransactionJournal $journal */
foreach ( $splitJournals as $journal ) {
$this -> makeMultiGroup ( $journal );
}
}
if ( 0 === $splitJournals -> count ()) {
2019-03-23 12:58:06 -05:00
$this -> info ( 'Found no split transaction journals. Nothing to do.' );
2019-03-23 02:10:59 -05:00
}
}
2019-03-17 11:05:16 -05:00
/**
* @ param TransactionJournal $journal
*
* @ throws Exception
*/
2019-03-23 02:10:59 -05:00
private function makeMultiGroup ( TransactionJournal $journal ) : void
2019-03-17 11:05:16 -05:00
{
// double check transaction count.
if ( $journal -> transactions -> count () <= 2 ) {
Log :: debug ( sprintf ( 'Will not try to convert journal #%d because it has 2 or less transactions.' , $journal -> id ));
return ;
}
Log :: debug ( sprintf ( 'Will now try to convert journal #%d' , $journal -> id ));
$this -> journalRepository -> setUser ( $journal -> user );
2019-04-06 01:10:50 -05:00
$this -> groupFactory -> setUser ( $journal -> user );
2019-03-17 11:05:16 -05:00
2019-03-23 12:58:06 -05:00
$data = [
2019-03-17 11:05:16 -05:00
// mandatory fields.
'group_title' => $journal -> description ,
'transactions' => [],
];
2019-03-23 12:58:06 -05:00
$destTransactions = $this -> getDestinationTransactions ( $journal );
$budgetId = $this -> journalRepository -> getJournalBudgetId ( $journal );
$categoryId = $this -> journalRepository -> getJournalCategoryId ( $journal );
$notes = $this -> journalRepository -> getNoteText ( $journal );
$tags = $this -> journalRepository -> getTags ( $journal );
$internalRef = $this -> journalRepository -> getMetaField ( $journal , 'internal-reference' );
2019-03-31 06:36:49 -05:00
$sepaCC = $this -> journalRepository -> getMetaField ( $journal , 'sepa_cc' );
$sepaCtOp = $this -> journalRepository -> getMetaField ( $journal , 'sepa_ct_op' );
$sepaCtId = $this -> journalRepository -> getMetaField ( $journal , 'sepa_ct_id' );
$sepaDb = $this -> journalRepository -> getMetaField ( $journal , 'sepa_db' );
$sepaCountry = $this -> journalRepository -> getMetaField ( $journal , 'sepa_country' );
$sepaEp = $this -> journalRepository -> getMetaField ( $journal , 'sepa_ep' );
$sepaCi = $this -> journalRepository -> getMetaField ( $journal , 'sepa_ci' );
$sepaBatchId = $this -> journalRepository -> getMetaField ( $journal , 'sepa_batch_id' );
2019-03-23 12:58:06 -05:00
$externalId = $this -> journalRepository -> getMetaField ( $journal , 'external-id' );
$originalSource = $this -> journalRepository -> getMetaField ( $journal , 'original-source' );
$recurrenceId = $this -> journalRepository -> getMetaField ( $journal , 'recurrence_id' );
$bunq = $this -> journalRepository -> getMetaField ( $journal , 'bunq_payment_id' );
2019-03-31 06:36:49 -05:00
$hash = $this -> journalRepository -> getMetaField ( $journal , 'import_hash' );
$hashTwo = $this -> journalRepository -> getMetaField ( $journal , 'import_hash_v2' );
2019-03-23 12:58:06 -05:00
$interestDate = $this -> journalRepository -> getMetaDate ( $journal , 'interest_date' );
$bookDate = $this -> journalRepository -> getMetaDate ( $journal , 'book_date' );
$processDate = $this -> journalRepository -> getMetaDate ( $journal , 'process_date' );
$dueDate = $this -> journalRepository -> getMetaDate ( $journal , 'due_date' );
$paymentDate = $this -> journalRepository -> getMetaDate ( $journal , 'payment_date' );
$invoiceDate = $this -> journalRepository -> getMetaDate ( $journal , 'invoice_date' );
2019-03-17 11:05:16 -05:00
2019-03-23 12:58:06 -05:00
Log :: debug ( sprintf ( 'Will use %d positive transactions to create a new group.' , $destTransactions -> count ()));
2019-03-17 11:05:16 -05:00
/** @var Transaction $transaction */
2019-03-23 12:58:06 -05:00
foreach ( $destTransactions as $transaction ) {
2019-03-17 11:05:16 -05:00
Log :: debug ( sprintf ( 'Now going to add transaction #%d to the array.' , $transaction -> id ));
2019-03-23 12:58:06 -05:00
$opposingTr = $this -> findOpposingTransaction ( $journal , $transaction );
2019-03-18 10:53:05 -05:00
if ( null === $opposingTr ) {
$this -> error (
sprintf (
'Journal #%d has no opposing transaction for transaction #%d. Cannot upgrade this entry.' ,
$journal -> id , $transaction -> id
)
);
continue ;
}
2019-03-17 11:05:16 -05:00
2019-03-18 10:53:05 -05:00
$tArray = [
2019-04-06 01:10:50 -05:00
'type' => strtolower ( $journal -> transactionType -> type ),
'date' => $journal -> date ,
'user' => $journal -> user_id ,
2019-03-17 11:05:16 -05:00
'currency_id' => $transaction -> transaction_currency_id ,
'foreign_currency_id' => $transaction -> foreign_currency_id ,
'amount' => $transaction -> amount ,
'foreign_amount' => $transaction -> foreign_amount ,
'description' => $transaction -> description ? ? $journal -> description ,
'source_id' => $opposingTr -> account_id ,
'destination_id' => $transaction -> account_id ,
'budget_id' => $budgetId ,
'category_id' => $categoryId ,
'bill_id' => $journal -> bill_id ,
2019-03-23 12:58:06 -05:00
'notes' => $notes ,
'tags' => $tags ,
'internal_reference' => $internalRef ,
2019-03-31 06:36:49 -05:00
'sepa_cc' => $sepaCC ,
'sepa_ct_op' => $sepaCtOp ,
'sepa_ct_id' => $sepaCtId ,
'sepa_db' => $sepaDb ,
'sepa_country' => $sepaCountry ,
'sepa_ep' => $sepaEp ,
'sepa_ci' => $sepaCi ,
'sepa_batch_id' => $sepaBatchId ,
2019-03-23 12:58:06 -05:00
'external_id' => $externalId ,
'original-source' => $originalSource ,
'recurrence_id' => $recurrenceId ,
'bunq_payment_id' => $bunq ,
2019-03-31 06:36:49 -05:00
'import_hash' => $hash ,
'import_hash_v2' => $hashTwo ,
2019-03-23 12:58:06 -05:00
'interest_date' => $interestDate ,
'book_date' => $bookDate ,
'process_date' => $processDate ,
'due_date' => $dueDate ,
'payment_date' => $paymentDate ,
'invoice_date' => $invoiceDate ,
2019-03-17 11:05:16 -05:00
];
$data [ 'transactions' ][] = $tArray ;
}
Log :: debug ( sprintf ( 'Now calling transaction journal factory (%d transactions in array)' , count ( $data [ 'transactions' ])));
2019-04-06 01:10:50 -05:00
$group = $this -> groupFactory -> create ( $data );
2019-03-17 11:05:16 -05:00
Log :: debug ( 'Done calling transaction journal factory' );
// delete the old transaction journal.
2019-03-23 02:10:59 -05:00
$this -> service -> destroy ( $journal );
2019-03-17 11:05:16 -05:00
// report on result:
2019-03-24 03:23:10 -05:00
Log :: debug (
2019-04-06 01:10:50 -05:00
sprintf ( 'Migrated journal #%d into group #%d with these journals: #%s' , $journal -> id , $group -> id , implode ( ', #' , $group -> transactionJournals -> pluck ( 'id' ) -> toArray ()))
2019-03-24 03:23:10 -05:00
);
$this -> line (
2019-04-06 01:10:50 -05:00
sprintf ( 'Migrated journal #%d into group #%d with these journals: #%s' , $journal -> id , $group -> id , implode ( ', #' , $group -> transactionJournals -> pluck ( 'id' ) -> toArray ()))
2019-03-24 03:23:10 -05:00
);
2019-03-17 11:05:16 -05:00
}
/**
*
*/
private function markAsMigrated () : void
{
2019-03-18 10:53:05 -05:00
app ( 'fireflyconfig' ) -> set ( self :: CONFIG_NAME , true );
2019-03-17 11:05:16 -05:00
}
}