#------------------------------------------------------------
# Copyright (C) 2009 RoyaltyShare, Inc.   All Rights Reserved
# $Id$
# $Id$
#------------------------------------------------------------

package RPS::ArtistRoyalty::Process::RunController;
use strict;
use Data::Dumper;
use File::Path;
use Time::HiRes qw(gettimeofday);

use lib '/app/tools/job/lib';
use Job::Status;

use lib '/app/tools/common/lib';
use Common::Assert;
use Common::Client;
use Common::RSApp;
use Common::Log;
use Common::DB::Item;

use lib '/app/tools/raptor/lib';
use Raptor::DB::Item::Sale;

use lib '/app/tools/rps/lib';
use RPS::DB::Item::ArtistRoyaltyStatement;
use RPS::ArtistRoyalty::Job::CreateStatement;
use RPS::Statement::Status;
use RPS::RoyaltyRun::Status;
#use RPS::ArtistRoyalty::ArtistRoyaltyRun;

use RPS::DB::Item::ArtistContractCrossedMap;
use RPS::DB::Item::ArtistPayee;
use RPS::DB::Item::ArtistPayeeAccount;
use RPS::DB::Item::ArtistRoyaltyAlbumBalanceAccount;
use RPS::DB::Item::ArtistRoyaltyRun;
use RPS::DB::Item::ArtistRoyaltyAlbum;
use RPS::DB::Item::ArtistRoyaltyExpenseItem;
use RPS::DB::Item::ArtistRoyaltyIncomeItem;
use RPS::DB::Item::ArtistRoyaltyLicenseIncomeItem;
use RPS::DB::Item::ArtistRoyaltyRunMissedSaleLog;
use RPS::DB::Item::ConsolidatedArtistRoyaltyRunMissedSaleLog;
use RPS::DB::Item::ArtistRoyaltyTransaction;
use RPS::DB::Item::SaleRunMap;
use RPS::DB::Item::ProductTrack;
use RPS::DB::Item::ReportQueries;
use RPS::DB::Item::ReportQueries::ArtistReservePipeline;
use RPS::DB::Item::NewArtistContract;
use RPS::DB::Item::TrackContract;
use RPS::DB::Item::AlbumContract;
use RPS::DB::Item::SaleArtistPayeeMap;
use RPS::DB::Item::PendingTransaction;
use RPS::DB::Item::FinanceTransaction;
use RPS::DB::Item::Expense;
use RPS::DB::Item::ExpenseArtistPayeeMap;
use RPS::DB::Item::ArtistContractTermReserveRun;
use RPS::DB::Item::ArtistContractTermReserve;
use RPS::DB::Item::ReserveArtistPayeeMap;
use RPS::DB::Item::Payor;
use RPS::DB::Item::Track;
use RPS::DB::Item::Product;
use RPS::DB::Item::LicenseIncomeArtistPayeeMap;
use RPS::DB::Item::ProductArtistPayeeMap;
use RPS::ArtistRoyalty::Process::CreateStatement;
use RPS::ArtistRoyalty::Job::CreatePDFStatements;
use RPS::ArtistRoyalty::Job::CreateExcelStatements;
use RPS::ArtistRoyalty::Job::CreateReservePipelineReport;
use RPS::Statement::Artist::PDF;
use RPS::ArtistRoyalty::Process::Exception;
use RPS::ArtistRoyalty::Job::CreateAlbumBalancesExportReportText;
use RPS::ArtistRoyalty::Job::CreateAlbumBalancesExportReportExcel;
use RPS::ArtistRoyalty::Job::CreateCurrentPeriodExportReportText;
use RPS::ArtistRoyalty::Job::CreateCurrentPeriodExportReportExcel;

use base 'RPS::ArtistRoyalty::Process';

use constant kSleepTime => 6;


sub _init
{
    my ($self, %args) = @_;

    # Logging is provided by the base class, among other things.
    #
    $self->SUPER::_init(%args);


    # Gotta have a run id, or we're wasting our time.
    #
    my $runID = $args{runID};
    assert($runID, "ERROR - runID is _required_");

    $self->{_runID} = $runID;


    # Get the run item, so we can grab the payorID for future reference.
    #
    my $run = $self->_getRunDBItem();
    $self->{_payorID} = $run->payor_id;
    $self->{_label}   = $run->label;

    return $self;
}


sub _getRunDBItem
{
    my ($self) = @_;
    assert($self->{_runID});

    my $dbItem = RPS::DB::Item::ArtistRoyaltyRun->Lookup(artist_royalty_run_id => $self->{_runID});
    return $dbItem;
}


sub commit
{
    my ($self) = @_;


    # !!! Let's keep the pre-flight sanity checking in place.
    #
    $self->_preCommitSanityCheck();

    my $allStatementItems = $self->_getAllStatements();
    while (my $statement = $allStatementItems->next())
    {
        $self->_commitStatement($statement);
    }

    # Tag all sales processed this run as 'pending'.
    #
    $self->_setSaleStatePending();

    # decrement the periods_remaining for the reserves we 'touched' this run.
    #
    $self->_adjustReservesPeriod();

    # mark run as committed
    #
    $self->_setStateToCommitted();


    # Mark previously committed run as closed.
    #
    my $otherCommittedRuns = $self->_getAllOtherCommittedRuns();
    if ($otherCommittedRuns)
    {
        while (my $otherRun = $otherCommittedRuns->next())
        {
            # !!! Also delete entries from 'artist_royalty_run_missed_sale_log' for this run id.
            #
            RPS::DB::Item::ArtistRoyaltyRunMissedSaleLog->DeleteByRunID($otherRun->artist_royalty_run_id());
            $otherRun->status(RPS::RoyaltyRun::Status::kClosed);
            $otherRun->save();

            # We also want to remove any remaining lines in the full 'missed sale log' table.
            # We'll keep the consolidated version around.
            #
            RPS::DB::Item::ArtistRoyaltyRunMissedSaleLog->DeleteByRunID($otherRun->artist_royalty_run_id);
        }
    }

    # Now re-create the pdfs and text statement files.
    # We want to do this after we set the run's state to committed, because
    # that will ensure these files have the 'final' appearance (i.e. no watermark).
    #
    $self->_createStatementFiles();

    # Last, we should clear out our run state the various mapping tables.   It was nice to keep these around while the run
    # was being evaluated, but now they are no longer necessary.
    #
    my $runID = $self->{_runID};

    RPS::DB::Item::ArtistContractCrossedMap->DeleteAllForRun($runID);
    RPS::DB::Item::SaleArtistPayeeMap->DeleteAllForRun($runID);
    RPS::DB::Item::ExpenseArtistPayeeMap->DeleteAllForRun($runID);
    RPS::DB::Item::ReserveArtistPayeeMap->DeleteAllForRun($runID);
    RPS::DB::Item::LicenseIncomeArtistPayeeMap->DeleteAllForRun($runID);
    RPS::DB::Item::ProductArtistPayeeMap->DeleteAllForRun($runID);


    return 0;
}


sub _createStatementFiles
{
    my ($self) = @_;

    $self->_createStatementJobs();
}


sub _createStatementJobs
{
    my ($self) = @_;


    # We'll create a seperate job for each statement.

    # Make sure the path exists
    #
    my $filePath = RPS::Statement::Artist::PDF::StatementPathFromRunID($self->{_runID});
    $self->_report("_createStatementJobs - filePath=$filePath", 3);
    if (! -d $filePath)
    {
        mkpath($filePath) or die RPS::ArtistRoyalty::Process::Exception->new("Unable to create path $filePath: $!");
    }


    # Create a job for all PDF statements, and put it in the queue.
    #
    my $clientID = Common::RSApp::GetClientID();

    my $jobArgs = RPS::ArtistRoyalty::Job::CreatePDFStatements->new
    (
        runID => $self->{_runID},
        clientID 	=> $clientID,
        filePath 	=> $filePath,
    );
    my $job = $jobArgs->enqueue();

    # Create a job for all Excel statements, and put it in the queue.
    #
    $jobArgs = RPS::ArtistRoyalty::Job::CreateExcelStatements->new
    (
        runID => $self->{_runID},
        clientID 	=> $clientID,
        filePath 	=> $filePath,
    );
    $job = $jobArgs->enqueue();
}


sub _createReservePipelineReport
{
    my ($self) = @_;

    $self->_report("*** adding reserve pipeline job to queue", 3);
    my $clientID = Common::RSApp::GetClientID();

    my $jobArgs = RPS::ArtistRoyalty::Job::CreateReservePipelineReport->new
    (
        runID 		=> $self->{_runID},
        clientID 	=> $clientID,
    );
    my $job = $jobArgs->enqueue();
    $self->_report("*** done adding reserve pipeline job to queue!", 3);
}

sub _createExportReports
{
    my ($self) = @_;

    my $clientID = Common::RSApp::GetClientID();

    # Make sure the path exists
    #
    my $filePath = RPS::Statement::Artist::PDF::StatementPathFromRunID($self->{_runID});
    $self->_report("_createExportReports - filePath=$filePath", 3);
    if (! -d $filePath)
    {
        mkpath($filePath) or die RPS::ArtistRoyalty::Process::Exception->new("Unable to create path $filePath: $!");
    }


    # Now create the report jobs
    #
    my $jobArgs = RPS::ArtistRoyalty::Job::CreateCurrentPeriodExportReportText->new
    (
        runID     => $self->{_runID},
        clientID  => $clientID,
        filePath  => $filePath,
    );
    my $job = $jobArgs->enqueue();


    my $jobArgs = RPS::ArtistRoyalty::Job::CreateCurrentPeriodExportReportExcel->new
    (
        runID     => $self->{_runID},
        clientID  => $clientID,
        filePath  => $filePath,
    );
    my $job = $jobArgs->enqueue();


    my $jobArgs = RPS::ArtistRoyalty::Job::CreateAlbumBalancesExportReportText->new
    (
        runID     => $self->{_runID},
        clientID  => $clientID,
        filePath  => $filePath,
    );
    my $job = $jobArgs->enqueue();


    my $jobArgs = RPS::ArtistRoyalty::Job::CreateAlbumBalancesExportReportExcel->new
    (
        runID     => $self->{_runID},
        clientID  => $clientID,
        filePath  => $filePath,
    );
    my $job = $jobArgs->enqueue();
}

########



sub delete
{
    my ($self) = @_;

    my $runID = $self->{_runID};
    assert($runID);

    # Get the run - we need to check it's status to make sure we can delete it.
    #
    my $run = RPS::DB::Item::ArtistRoyaltyRun->Lookup(artist_royalty_run_id => $runID);
    if (! $run)
    {
        die RPS::ArtistRoyalty::Process::Exception->new("ERROR - cannot find artist royalty run with id $runID");
    }


    if (RPS::RoyaltyRun::Status::kCommitted == $run->status)
    {
        die RPS::ArtistRoyalty::Process::Exception->new("ERROR - run $runID has already been committed, and cannot be deleted");
    }


	# Delete the reserves.
	#
    RPS::DB::Item::ArtistContractTermReserve->DeleteByArtistRoyaltyRunID($runID);
    RPS::DB::Item::ArtistContractTermReserveRun->DeleteByArtistRoyaltyRunID($runID);

    # Get all of the statements created by this run,
    # and delete them (and their children!).
    #
    my $statements = RPS::DB::Item::ArtistRoyaltyStatement->GetByArtistRoyaltyRunIDForDeletion($runID);

    while ($statements->hasNext())
    {
        my $statement = $statements->next();
        $self->_deleteStatement($statement);
    }

    # Delete the pdf statements
    #
    my $pdfPath = RPS::Statement::Artist::PDF::StatementPathFromRunID($runID);
    if (-d $pdfPath)
    {
        system("rm -rf $pdfPath");
    }

    # Delete this run from the run table.
    #
    $run->delete();

    # Delete sale map entries
    #
    RPS::DB::Item::SaleRunMap->DeleteByArtistRoyaltyRunID($runID);

    # Delete all mapping tables used for run
    #
    RPS::DB::Item::ArtistContractCrossedMap->DeleteAllForRun($runID);
    RPS::DB::Item::SaleArtistPayeeMap->DeleteAllForRun($runID);
    RPS::DB::Item::ExpenseArtistPayeeMap->DeleteAllForRun($runID);
    RPS::DB::Item::ReserveArtistPayeeMap->DeleteAllForRun($runID);
    RPS::DB::Item::LicenseIncomeArtistPayeeMap->DeleteAllForRun($runID);
    RPS::DB::Item::ProductArtistPayeeMap->DeleteAllForRun($runID);


    # Delete the reports
    #
    RPS::DB::Item::ArtistRoyaltyRunMissedSaleLog->DeleteByRunID($runID);
    RPS::DB::Item::ConsolidatedArtistRoyaltyRunMissedSaleLog->DeleteByRunID($runID);

    RPS::DB::Item::ReportQueries::ArtistReservePipeline->DeleteByArtistRoyaltyRunID($runID);

    return 0;
}


sub _deleteStatement
{
    my ($self, $statement) = @_;

    my $statementID = $statement->artist_royalty_statement_id;

    $self->_report("deleting statement $statementID", 2);
    $statement->delete();

    $self->_report("  ... deleting transactions",3);
    RPS::DB::Item::ArtistRoyaltyTransaction->DeleteByArtistRoyaltyStatementID($statementID);

    $self->_report("  ... deleting license income items",3);
    RPS::DB::Item::ArtistRoyaltyLicenseIncomeItem->DeleteByArtistRoyaltyStatementID($statementID);

    my $albums = RPS::DB::Item::ArtistRoyaltyAlbum->GetByArtistRoyaltyStatementID($statementID);
    while ($albums->hasNext())
    {
        my $album = $albums->next();
        my $albumID = $album->artist_royalty_album_id;

        $self->_report("  ... deleting expense items",3);
        RPS::DB::Item::ArtistRoyaltyExpenseItem->DeleteByArtistRoyaltyAlbumID($albumID);

        $self->_report("  ... deleting income items",3);
        RPS::DB::Item::ArtistRoyaltyIncomeItem->DeleteByArtistRoyaltyAlbumID($albumID);
    }

    $self->_report("  ... deleting album items",3);
    RPS::DB::Item::ArtistRoyaltyAlbum->DeleteByArtistRoyaltyStatementID($statementID);

    return 0;
}




#########


sub run
{
    my ($self) = @_;

    return unless( $self->SUPER::run() );
    

    $self->_setStateToRunning();

    # Create Statement database records, and
    # queue up CreateStatement jobs.
    #
    $self->_createStatementsAndJobs();


    # Now we wait.
    # Our 'child' processes won't necessarily be running on this host,
    # so we can't use any fancy waitpid sort of interface.
    # We're forced to poll, and wait until everybody is finished.
    #
    # If there is an error in one of our child processes, we can attempt
    # to retry it.  Getting those semantics correct will be interesting.
    #
    my @statementIDs;
    my $badCount = 0;
    my $numJobsRunning = scalar (keys %{$self->{_jobs}});
    while ($numJobsRunning)
    {
        sleep(kSleepTime);

        my $newCount = 0;

        # Iterate through the jobs, and see if they are still running.
        #
        foreach my $jobKey (keys %{$self->{_jobs}})
        {
            my $jobRecord = $self->{_jobs}{$jobKey};
            if (! $jobRecord->{complete})
            {
                my $job = $jobRecord->{job};
                my $jobStatus = $job->getStatus();

                if (Job::Status::kComplete eq $jobStatus
                 || Job::Status::kDead eq $jobStatus)
                {

                    # So what _really_ happened?
                    # !!! Starting to wonder if perhaps we should 'share' status codes?
                    #
                    my $statementID = $job->getCommandLineArg('statementID');
                    my $statement = $self->_getStatement($statementID);

                    Common::Log::Print("Job state for statement $statementID : job status $jobStatus - statement status " . $statement->status);

                    if (RPS::Statement::Status::kComplete == $statement->status)
                    {
                        $jobRecord->{complete} = 1;

                        # Store the statement id so that we can queue up the pdf job later.
                        # For now, we'll assume all statements are MCPS.
                        push(@statementIDs, $statementID);
                    }
                    else
                    {
                        # !!! Do we want to try and re-queue?  Perhaps someday, but for now we just fail.
                        #
                        $self->_report("job for statement $statementID did not complete successfully - reported job status = " . $jobStatus, 2);
                        $jobRecord->{complete} = 1;
                        $jobRecord->{error} = $statement->status;
                        $badCount++;
                    }
                }
                else
                {
                    # It's still waiting/running/etc.
                    #
                    $newCount++;
                }
            }
        }

        $numJobsRunning = $newCount;
    }


    $self->_report("All jobs reported - bad count = $badCount", 2);

    # Update the run table with our final status, and clean up.
    #
    if ($badCount > 0)
    {
        $self->_setStateToError();
    }
    else
    {
        # Build the consolidated missed sale log
        #
        RPS::DB::Item::ConsolidatedArtistRoyaltyRunMissedSaleLog->BuildForRun($self->{_runID});

        # Mark as complete so users can view statements
        #
        $self->_setStateToComplete();

        # Create the PDF statement jobs.
        #
        $self->_createStatementFiles();

        # Create the reserve pipeline report.
        #
        $self->_createReservePipelineReport();

        # Create the royalty export reports
        #
        $self->_createExportReports();
    }

    # Return '0' to signify a normal exit
    #
    return 0;
}


sub _setStateToRunning
{
    my ($self) = @_;

    # Check the run's state - It should be in 'waitingToRun'.
    # Update the record to indicate we're taking over.
    #
    my $dbItem = $self->_getRunDBItem();

    die RPS::ArtistRoyalty::Process::Exception->new("runID " . $self->{_runID} . " is not in the 'waiting' state")
     unless RPS::RoyaltyRun::Status::kWaitingToRun == $dbItem->status();

    $dbItem->start_time(Common::DB::Item::kDateTimeNow);
    $dbItem->pid($$);
    $dbItem->hostname(Common::RSApp::GetHostname());
    $dbItem->status(RPS::RoyaltyRun::Status::kRunning);

    $dbItem->save();
}


sub _setStateToError
{
    my ($self) = @_;

    $self->_report("setting state to error");

    my $dbItem = $self->_getRunDBItem();

    $dbItem->end_time(Common::DB::Item::kDateTimeNow);
    $dbItem->status(RPS::RoyaltyRun::Status::kError);

    $dbItem->save();
}


sub _setStateToComplete
{
    my ($self) = @_;

    $self->_report("setting state to complete");
    my $dbItem = $self->_getRunDBItem();

    $dbItem->end_time(Common::DB::Item::kDateTimeNow);
    $dbItem->status(RPS::RoyaltyRun::Status::kComplete);

    $dbItem->save();
}


sub _setStateToCommitted
{
    my ($self) = @_;
    $self->_report("setting state to committed");

    my $dbItem = $self->_getRunDBItem();

    $dbItem->status(RPS::RoyaltyRun::Status::kCommitted);
    $dbItem->save();
}


sub _createStatementsAndJobs
{
    my ($self) = @_;

    # Before we kick off the individual payee jobs, we
    # need to 'pre-scan' the sale table.
    # For huge customers, I don't want to have every job looking at every sale.
    # So we will do a quick (15 minutes or so for our biggest clients) sorting process.
    #
    $self->_createSalePayeeMapping();

    $self->_createLicenseIncomePayeeMapping;

    $self->_createExpensePayeeMapping();

    $self->_createReservePayeeMapping();

    $self->_buildCrossTable();


    # The default behavior will be to create a job for each active artist payee.
    #
    my @activePayees = $self->_getAllActivePayeeIDs();


    $self->_report("_createStatementsAndJobs: Number of payees = " . scalar @activePayees, 4);

    foreach my $payeeID (@activePayees)
    {
        $self->_addJobToJobList($payeeID, $self->_createPayeeStatementJob($payeeID));
    }
}


sub _buildCrossTable
{
    my ($self) = @_;

    my %albumContractHash;

    my $albumContracts = RPS::DB::Item::AlbumContract->GetCrossCollateralized();
    while (my $albumContract = $albumContracts->next())
    {
        next unless RPS::DB::Item::AlbumContract::kStatusActive == $albumContract->status();

        my $artistContract = RPS::DB::Item::NewArtistContract->Lookup(artist_contract_id => $albumContract->artist_contract_id);

        next unless $artistContract->payor_id() == $self->{_payorID};

        $albumContractHash{$albumContract->album_id}{$artistContract->artist_contract_id} = 1;
    }

    my $trackContracts = RPS::DB::Item::TrackContract->GetCrossCollateralized();
    while (my $trackContract = $trackContracts->next())
    {
        next unless RPS::DB::Item::TrackContract::kStatusActive == $trackContract->status();

        my $track = RPS::DB::Item::Track->Lookup(track_id => $trackContract->track_id);
        my $artistContract = RPS::DB::Item::NewArtistContract->Lookup(artist_contract_id => $trackContract->artist_contract_id);

        next unless $artistContract->payor_id() == $self->{_payorID};

        $albumContractHash{$track->album_id}{$artistContract->artist_contract_id} = 1;
    }

    foreach my $albumID (keys %albumContractHash)
    {
        foreach my $contractID (keys %{$albumContractHash{$albumID}})
        {
            $self->_report("creating crossed album contract entry for album id " . $albumID . " contract id $contractID", 4);
            my $newMapItem = RPS::DB::Item::ArtistContractCrossedMap->Create
            (
                run_id => $self->{_runID},
                album_id => $albumID,
                artist_contract_id => $contractID,
            );
            $newMapItem->save();
        }
    }

}


sub _createExpensePayeeMapping
{
    my ($self) = @_;

    my $payorID = $self->{_payorID};

    my $expenses = RPS::DB::Item::Expense->GetAllUnprocessed();
    while (my $expense = $expenses->next())
    {
        $self->_report("Expense: " . Dumper($expense), 3);

        # Get the artist_contract_id from the proper contract
        #

        my $artistContract;
        my $albumID = 0;
        my $trackID = 0;
        my $contract_type = $expense->parent_type;

        if (RPS::DB::Item::Expense::kParentAlbumContract == $contract_type)
        {
            my $albumContract = RPS::DB::Item::AlbumContract->Lookup(album_contract_id => $expense->parent_id);
            if (! $albumContract)
            {
                $self->_report("skipping expense  " . $expense->expense_id . " - cannot find album contract", 3);
                next;
            }
            if (RPS::DB::Item::AlbumContract::kStatusActive != $albumContract->status())
            {
                $self->_report("skipping expense  " . $expense->expense_id . " - album contract is not active", 3);
                next;
            }
            $artistContract = RPS::DB::Item::NewArtistContract->Lookup(artist_contract_id => $albumContract->artist_contract_id);
            $albumID = $albumContract->album_id;
        }
        elsif (RPS::DB::Item::Expense::kParentTrackContract == $contract_type)
        {
            my $trackContract = RPS::DB::Item::TrackContract->Lookup(track_contract_id => $expense->parent_id);
            if (! $trackContract)
            {
                $self->_report("skipping expense ". $expense->expense_id . " - cannot find track contract", 3);
                next;
            }
            if (RPS::DB::Item::TrackContract::kStatusActive != $trackContract->status())
            {
                $self->_report("skipping expense  " . $expense->expense_id . " - track contract is not active", 3);
                next;
            }

            $artistContract = RPS::DB::Item::NewArtistContract->Lookup(artist_contract_id => $trackContract->artist_contract_id);
            $trackID = $trackContract->track_id;
            my $track = RPS::DB::Item::Track->Lookup(track_id => $trackID);
            $albumID = $track->album_id;
        }


        # Skip this expense if the contract belongs to a payor we are not looking at.
        #
        if ($artistContract->payor_id != $payorID)
        {
            $self->_report("skipping expense " . $expense->expense_id . " - not the right payor", 3);
           next;
        }

        my $payeeID = $artistContract->artist_payee_id;

        $self->_report("creating expense payee mapping for payee $payeeID expense " . $expense->expense_id, 4);
        my $newMapItem = RPS::DB::Item::ExpenseArtistPayeeMap->Create
        (
            run_id => $self->{_runID},
            expense_id => $expense->expense_id,
            artist_payee_id => $payeeID,
            artist_contract_id => $artistContract->artist_contract_id,
            album_id => $albumID,
            track_id => $trackID,

        );
        $newMapItem->save();

    }

}


sub _createLicenseIncomePayeeMapping
{
    my ($self) = @_;

    my $payorID = $self->{_payorID};

    $self->_report("--- processing license income sales", 2);

#    my $run = new RPS::ArtistRoyalty::ArtistRoyaltyRun( artistRoyaltyRunID => $self->{_runID} );
#    my $run = RPS::ArtistRoyalty::ArtistRoyaltyRun->new( artistRoyaltyRunID => $self->{_runID} );
	
	my $run = $self->_getRunDBItem();

    my $sales = Raptor::DB::Item::Sale->GetUnprocessedLicenseIncomeRoyaltySales( ending_sale_date => $run->ending_sale_date() );
    $self->_report("unprocess sales count: " . $sales->size, 3);

    while (my $sale = $sales->next())
    {
        $self->_report("SALE: " . Dumper($sale), 4);


        # Get the original license income data.
        #
        my $licenseIncomeData = $self->_getLicenseIncomeData($sale->sale_id);
        if (! $licenseIncomeData)
        {
            RPS::ArtistRoyalty::Process::CreateStatement::logMissedSale($self->{_runID}, $sale, 'Missing license income data', details => 'License income');
            $self->_report("Skipping sale_id " . $sale->sale_id . " : cannot find license income data", 3);
            my $mi = RPS::DB::Item::SaleRunMap->Create
            (
                sale_id => $sale->sale_id,
                run_id => $self->{_runID},
                run_type => RPS::DB::Item::SaleRunMap::kRunTypeArtistRoyalty,
                status => RPS::DB::Item::SaleRunMap::kStatusNoLicenseIncomeData,
            );
            $mi->save();
            next;
        }

        my $albumID = $licenseIncomeData->album_id;
        my $trackID = $licenseIncomeData->track_id;

        # Check the album's 'inactive' flag.
        #
        if ($albumID)
        {
            my $albumData = RPS::DB::Item::Album->Lookup(album_id => $albumID);
            if ($albumData->inactive() || $albumData->status() == 0)
            {
                $self->_report("skipping sale_id " . $sale->sale_id . " : album $albumID is inactive", 3);
                RPS::ArtistRoyalty::Process::CreateStatement::logMissedSale($self->{_runID}, $sale, "Album inactive", albumID => $albumID, details => 'License income');
                my $mi = RPS::DB::Item::SaleRunMap->Create
                (
                    sale_id => $sale->sale_id,
                    run_id => $self->{_runID},
                    run_type => RPS::DB::Item::SaleRunMap::kRunTypeArtistRoyalty,
                    status => RPS::DB::Item::SaleRunMap::kStatusInactiveAlbum,
                );
                $mi->save();
                next;
            }
        }

        my @contracts;
        if ($licenseIncomeData->contract_id)
        {
            my $contract = RPS::DB::Item::NewArtistContract->Lookup(artist_contract_id => $licenseIncomeData->contract_id);

            # Check the payor...
            #
            if ($contract->payor_id != $payorID)
            {
                $self->_report("skipping sale_id " . $sale->sale_id . " : associated contract does not match payor $payorID", 3);
                next;
            }

            # !!! Sanity-check this.
            # !!! The contract MIGHT NOT be associated with this album and/or track anymore.
            #
            if ($albumID)
            {
                my $contracts = RPS::DB::Item::NewArtistContract->GetByPayorAlbumTrack($payorID, $albumID, $trackID);
                my $foundIt = 0;
                while (my $testContract = $contracts->next())
                {
                    if ($testContract->artist_contract_id == $contract->artist_contract_id)
                    {
                        $foundIt = 1;
                        last;
                    }
                }

                if (! $foundIt)
                {
                    $self->_report("skipping sale_id " . $sale->sale_id . " : contract " . $licenseIncomeData->contract_id . " does not appear to be associated with this album/track anymore", 3);
                    RPS::ArtistRoyalty::Process::CreateStatement::logMissedSale($self->{_runID}, $sale, "Contract no longer matches", albumID => $albumID, contract => $contract, details => 'License income');
                    next;
                }
            }
            push @contracts, $contract;
        }
        else
        {
            # Fetch the artist contracts that match the provided album and/or track id.
            # !!! I don't want contracts associated with inactive albums.
            #
            #my $contracts = RPS::DB::Item::NewArtistContract->GetByPayorAlbumTrack($payorID, $albumID, $trackID);
            # Going to grab matching contracts from all payors.
            #
            my $contracts = RPS::DB::Item::NewArtistContract->GetByAlbumTrack($albumID, $trackID);
            while (my $c = $contracts->next())
            {
                push @contracts, $c;
            }
        }

        $albumID = 0 unless $albumID;
        $trackID = 0 unless $trackID;

        if (! scalar @contracts)
        {
            $self->_report("skipping sale_id " . $sale->sale_id . " : no matching contracts found", 3);
            RPS::ArtistRoyalty::Process::CreateStatement::logMissedSale($self->{_runID}, $sale, "No matching contracts", albumID => $albumID, trackID => $trackID, details => 'License income');
            next;
        }

        # Build a hash that contains the contract ids that have already paid on this sale.
        # !!! Maybe I can re-use the current method?
        #
        my $previousContracts = $self->_getPaidContractIDsForLicenseIncome($sale);
# !!! inline calls to dumper are expensive, and even if the log level < 4 Dumper still gets called here.
#        $self->_report("previous contracts: " . Dumper($previousContracts), 4);

        foreach my $contract (@contracts)
        {
            # First, make sure the contract is associated with the correct payor.
            #
            if ($contract->payor_id != $payorID)
            {
                next;
            }

            # !!! CHECK to see whether we've paid on this contract already...
            #
            if ($previousContracts->{$contract->artist_contract_id}{$trackID})
            {
                $self->_report("skipping contract " . $contract->artist_contract_id . " : already paid", 3);
                next;
            }

            my $payeeID = $contract->artist_payee_id;

            # And now make sure the sale falls within the date range of the contract
            my $saleOutOfRange = 0;
            if ($contract->term_start)
            {
                if ($sale->date_end lt $contract->term_start)
                {
                    $saleOutOfRange = 1;
                }

            }

            if ($contract->term_end)
            {
                if ($sale->date_end gt $contract->term_end)
                {
                    $saleOutOfRange = 1;
                }
            }

            if ($saleOutOfRange == 1)
            {
                $self->_report("skipping sale for contract " . $contract->artist_contract_id . " - date out of term range", 3);
                RPS::ArtistRoyalty::Process::CreateStatement::logMissedSale($self->{_runID}, $sale, "Date out of range", details => 'Sale date does not occur within term date range of contract', trackID => $trackID, albumID => $albumID, contract => $contract );
                next;
            }

            $self->_report("creating license income payee mapping for payee $payeeID license_income " . $licenseIncomeData->license_income_id, 4);

            my $newMapItem = RPS::DB::Item::LicenseIncomeArtistPayeeMap->Create
            (
                run_id => $self->{_runID},
                license_income_id => $licenseIncomeData->license_income_id,
                artist_payee_id => $payeeID,
                artist_contract_id => $contract->artist_contract_id,
            );
            $newMapItem->save();
        }

    }

}


sub _getLicenseIncomeData
{
    my ($self, $saleID) = @_;

    my $data = RPS::DB::Item::LicenseIncome->Lookup(sale_id => $saleID);
    return $data;
}


sub _getPaidContractIDsForLicenseIncome
{
    my ($self, $sale) = @_;

    my %contractHash;

    my $saleID = $sale->sale_id;
    $self->_report("looking for paid contracts for sale $saleID", 4);

    my $mapItems = RPS::DB::Item::SaleRunMap->GetPaidLicenseIncomeArtistRoyaltyBySaleID($saleID);
    while (my $mapItem = $mapItems->next())
    {
        my $trackID = 0;
        my $contractID = 0;

        # Not all of these entries will _have_ a statement item id.
        # Data we pulled from the old sale_artist_royalty_run_map table, for example.
        #
        next unless $mapItem->statement_item_id;

        my $originalLicenseIncomeItem = RPS::DB::Item::ArtistRoyaltyLicenseIncomeItem->Lookup(artist_royalty_license_income_item_id => $mapItem->statement_item_id);
        if (! $originalLicenseIncomeItem)
        {
            $self->_report("ERROR - cannot find original license income item for sale map item: " . Dumper($mapItem));
            next;
        }

        # We want to determine the _track_ that was associated with this payment, if any.
        # This is because in theory the same contract can be attached to multiple tracks on
        # a single album.  So, we want to make sure we are able to distinguish between new
        # track-contracts and old track-contracts for album level sales.
        #
        if ($originalLicenseIncomeItem->track_id)
        {
            $trackID = $originalLicenseIncomeItem->track_id;
        }
        $contractID = $originalLicenseIncomeItem->artist_contract_id;

        $contractHash{$contractID}{$trackID} = 1;
    }

    return \%contractHash;
}


sub _createReservePayeeMapping
{
    my ($self) = @_;

    my $payorID = $self->{_payorID};

    # Now, we need to deal with reserves that are being liquidated.
    #
    my $reserves = RPS::DB::Item::ArtistContractTermReserve->GetAllCommitted();
    while (my $reserve = $reserves->next())
    {
# !!! Really, we should look at replacing the 'log level' with the newer Common::Log level mechanism
#        $self->_report("Reserve: " . Dumper($reserve), 3);

        my $termID = $reserve->artist_contract_term_id;

        # Need to fetch the contract id, from the term.
        #
        my $term = RPS::DB::Item::NewArtistContractTerm->Lookup(artist_contract_term_id => $termID);
        if (! $term)
        {
            $self->_report("skipping reserve" . $reserve->artist_contract_term_reserve_id . " - term $termID cannot be found!", 3);
            next;
        }
        my $contract = RPS::DB::Item::NewArtistContract->Lookup(artist_contract_id => $term->artist_contract_id);


        # Skip this reserve if payor_id is not correct.
        #
        if ($contract->payor_id != $payorID)
        {
            $self->_report("skipping reserve" . $reserve->artist_contract_term_reserve_id . " - not the right payor", 3);
            next;
        }

        my $payeeID = $contract->artist_payee_id;

        $self->_report("creating reserve payee mapping for payee $payeeID reserve " . $reserve->artist_contract_term_reserve_id, 4);
        my $newMapItem = RPS::DB::Item::ReserveArtistPayeeMap->Create
        (
            run_id => $self->{_runID},
            reserve_id => $reserve->artist_contract_term_reserve_id,
            artist_payee_id => $payeeID,
        );
        $newMapItem->save();

    }

}


sub _pruneMissedSaleLog
{
    my ($self) = @_;

    $self->_report("pruning the missed sale log", 3);

    # Let's get the list of sales in the log for this run
    #
    my $missedSales = RPS::DB::Item::ArtistRoyaltyRunMissedSaleLog->GetSaleIDsByRunID($self->{_runID});
    while (my $missedSale = $missedSales->next())
    {
        # Now check to see if the sale has been marked as paid for this run.
        #
        $self->_report("checking to see if sale " . $missedSale->sale_id . " has been marked as paid for this run", 4);

        my $paidSales = RPS::DB::Item::SaleRunMap->GetPaidArtistRoyaltyBySaleIDAndRunID($missedSale->sale_id, $self->{_runID});
        my $paidCount = $paidSales->size();

        if ($paidCount > 0)
        {
            $self->_report("deleting entries for sale " . $missedSale->sale_id . " run id " . $self->{_runID}, 4);
            # If we have any entries for this sale, delete them.
            RPS::DB::Item::ArtistRoyaltyRunMissedSaleLog->DeleteByRunIDAndSaleID($self->{_runID}, $missedSale->sale_id);
        }
        else
        {
            # If we have ever paid out on this sale, don't show the "No matching contracts" messages
            #
            $self->_report("checking to see if sale " . $missedSale->sale_id . " has ever been marked as paid", 4);
            my $everPaidSales = RPS::DB::Item::SaleRunMap->GetPaidArtistRoyaltyBySaleID($missedSale->sale_id);
            my $everPaidCount = $everPaidSales->size();
            if ($everPaidCount > 0)
            {
                $self->_report("deleting 'No matching contracts' entries for sale " . $missedSale->sale_id . " run id " . $self->{_runID}, 4);
                # If we have any entries for this sale, delete them.
                RPS::DB::Item::ArtistRoyaltyRunMissedSaleLog->DeleteByRunIDSaleIDAndReason($self->{_runID}, $missedSale->sale_id, "No matching contracts");
            }
        }

    }


}


sub _createSalePayeeMapping
{
    my ($self) = @_;

    $self->_report("_createSalePayeeMapping", 3);


    # To speed this up, we're going to cache a few tables.
    #
    my %productCache;
    my $allProducts = RPS::DB::Item::Product->GetAll();
    while (my $product = $allProducts->next())
    {
        $productCache{$product->product_id()} = $product;
    }

    my %trackIDsByAlbumCache;
    my %trackCache;
    my $allTracks = RPS::DB::Item::Track->GetAll();
    while (my $track = $allTracks->next())
    {
        my $trackID = $track->track_id();
        my $albumID = $track->album_id();
        push @{$trackIDsByAlbumCache{$albumID}}, $trackID;
        $trackCache{$trackID} = $track;
    }


    # Limit this using our payor id
    # !!! Let's not - Get them all, we'll sort them by payor_id later.
    my %artistContractCache;
    my $allArtistContracts = RPS::DB::Item::NewArtistContract->GetAll();
    while (my $contract = $allArtistContracts->next())
    {
        $artistContractCache{$contract->artist_contract_id()} = $contract;
    }

    my %allTrackContracts;
    my $allTContracts = RPS::DB::Item::TrackContract->GetAll();
    while (my $tc = $allTContracts->next())
    {
        push @{$allTrackContracts{$tc->track_id()}}, $tc;
    }

    
    my %allAlbumContracts;
    my $allAContracts = RPS::DB::Item::AlbumContract->GetAll();
    while (my $ac = $allAContracts->next())
    {
        push @{$allAlbumContracts{$ac->album_id()}}, $ac;
    }



    # Now build a mapping of product_id to payee_id.
    # !!! They want to know when a product has NO matching contracts for ANY payor.
    # !!! So in building this data-structure, we're going to want to key by payor_id, so 
    # !!! we will be able to quickly tell.  Probably {product_id}{payor_id}{payee_id}
    #
    # !!! Will this _eliminate_ the GetMatchingContracts call?  That would be good...
    #
    my %productPayeeMap;
    my %trackPayeeMap;   
    foreach my $productID (keys %productCache)
    {
        my @albums;
        my @tracks;

        my $product = $productCache{$productID};
        my $productTypeID = $product->product_type_id;
        my $productType = $self->_getProductType($productTypeID);

        if (RPS::DB::Item::Product::kProductTypeDigitalTrack == $product->product_type_id)
        {
            # asset id is for a track
            #
            push(@tracks, $product->asset_id);
            my $trackData = $trackCache{$product->asset_id};

            push(@albums, $trackData->album_id);
        }
        else
        {
            # asset id is for an album
            #
            push(@albums, $product->asset_id);

            my $trackIDs = $trackIDsByAlbumCache{$product->asset_id};
            if ($trackIDs)
            {
                push @tracks, @$trackIDs;
            }
        }

        # Okay, now go through the tracks, grab the contracts, and store the payee id
        # if the payor is the same as the one for this run.
        #
        foreach my $trackID (@tracks)
        {
            my $trackContracts = $allTrackContracts{$trackID};
            next unless $trackContracts;

            foreach my $trackContract (@$trackContracts)
            {
                # If this track contract is in our cache for the correct payor,
                # then let's grab the payee id.
                #
                my $cachedContract = $artistContractCache{$trackContract->artist_contract_id()};
                if ($cachedContract)
                {
                    my $payorID = $cachedContract->payor_id;
                    my $payeeID = $cachedContract->artist_payee_id;

                    $productPayeeMap{$productID}{$payorID}{$payeeID} = 1;
                    $trackPayeeMap{$trackID}{$payorID}{$payeeID} = 1;
                }
            }
        }


        # Now do the same thing for albums.
        #
        foreach my $albumID (@albums)
        {
            my $albumContracts = $allAlbumContracts{$albumID};

            foreach my $albumContract (@$albumContracts)
            {
                # If this album contract is in our cache for the correct payor,
                # then let's grab the payee id.
                #
                my $cachedContract = $artistContractCache{$albumContract->artist_contract_id()};
                if ($cachedContract)
                {
                    my $payorID = $cachedContract->payor_id;
                    my $payeeID = $cachedContract->artist_payee_id;

                    $productPayeeMap{$productID}{$payorID}{$payeeID} = 1;
                }
            }
        }
    }



    # !!! Move this out here - This list doesn't change...
    #
    my $allPayorIDs = RPS::ArtistRoyalty::Process::CreateStatement::getAllPayorIDs();;
    

    # Once we've mapped a product, we don't need to map it again.
    # This hash keeps track of that (instead of querying the db)
    #
    my %mappedProductIDs;

	my $run = $self->_getRunDBItem();

	my $allSales = Raptor::DB::Item::Sale->GetUnprocessedRoyaltySales( ending_sale_date => $run->ending_sale_date() );
    $self->_report("_createSalePayeeMapping - scanning sales: count= " . $allSales->size(), 3);
    while ($allSales->hasNext())
    {
        my $sale = $allSales->next();
        my $saleID = $sale->sale_id();


        $self->_report("looking at sale id " . $sale->sale_id, 4);

        if (! $sale->product_id() && ! $sale->lic_track_id() )
        {
            $self->_report("  skipping - no product id or track id", 3);
            RPS::ArtistRoyalty::Process::CreateStatement::logMissedSale($self->{_runID}, $sale, 'No track id or product id');

            my $mi = RPS::DB::Item::SaleRunMap->Create
            (
                sale_id => $sale->sale_id,
                run_id => $self->{_runID},
                run_type => RPS::DB::Item::SaleRunMap::kRunTypeArtistRoyalty,
                status => RPS::DB::Item::SaleRunMap::kStatusNoLicTrackOrProductID,
            );
            $mi->save();
            next;
        }


        # We've already figured out which payee_ids map to all products and tracks...
        # Now we just need to write to the proper tables.
        #

        if ($sale->product_id() && ! $mappedProductIDs{$sale->product_id()})
        {
            my $productID = $sale->product_id();
            
            # One was or another this product is getting dealt with, so we don't need
            # to look at it again after this...
            #
            $mappedProductIDs{$productID} = 1;


            # Is this a bogus product_id?
            #
            if (! $productCache{$productID})
            {
                $self->_report("  skipping - product not in cache", 3);
                RPS::ArtistRoyalty::Process::CreateStatement::logMissedSale($self->{_runID}, $sale, 'Bad product id');

                my $mi = RPS::DB::Item::SaleRunMap->Create
                (
                    sale_id => $sale->sale_id,
                    run_id => $self->{_runID},
                    run_type => RPS::DB::Item::SaleRunMap::kRunTypeArtistRoyalty,
                    status => RPS::DB::Item::SaleRunMap::kStatusBadProductID,
                );
                $mi->save();

                next;
            }


            # Get the 'payor_id' hash, if there is one.
            # !!! Basically, if we don't have an entry in the productPayeeMap, then there are no 
            # !!! contracts associated with this product, and we can log that right now.
            #
            my $productPayorHash = $productPayeeMap{$productID};
            if (! $productPayorHash)
            {
                # No contracts at all, for any payor, for that product...
                #
                $self->_report("...no matching contracts found, skipping sale " . $sale->sale_id, 3);

                my $product = $productCache{$productID};
                RPS::ArtistRoyalty::Process::CreateStatement::logMissedSale($self->{_runID}, $sale, 'No matching contracts', product => $productCache{$productID});

                my $mi = RPS::DB::Item::SaleRunMap->Create
                (
                    sale_id => $sale->sale_id,
                    run_id => $self->{_runID},
                    run_type => RPS::DB::Item::SaleRunMap::kRunTypeArtistRoyalty,
                    status => RPS::DB::Item::SaleRunMap::kStatusNoMatchingContracts,
                );
                $mi->save();
            }
            else
            {
                # We have contracts, but do they match our payor id?
                # Not an error if they don't... Just iterate over our payor_ids, and make a sale artist payee map entry for each.
                # !!! Right now there will only be one payor_id... But keep this flexible just in case...
                #

                # We'll keep track of 'seen payees', because really only need to map each payee once, 
                # but there may be multiple contracts for that payee in our payeeIDHash.
                #
                my %seenPayeeIDs;
                foreach my $payorID (@$allPayorIDs)
                {
                    my $payeeIDHash = $productPayeeMap{$productID}{$payorID};
                    if ($payeeIDHash)
                    {
                        foreach my $payeeID (keys %$payeeIDHash)
                        {
                            next if $seenPayeeIDs{$payeeID};
                            $self->_report("creating product map entry for sale id " . $sale->sale_id . " payee id $payeeID product id $productID", 4);
                            my $newMapItem = RPS::DB::Item::ProductArtistPayeeMap->Create
                            (
                                run_id => $self->{_runID},
                                product_id => $productID,
                                artist_payee_id => $payeeID,
                            );
                            $newMapItem->save();

                            $seenPayeeIDs{$payeeID} = 1;
                        }
                    }
                }
            }
        }
        elsif ($sale->lic_track_id())
        {
            #
            # !!! THIS IS EXACTLY THE SAME AS THE ABOVE BLOCK. JUST REPLACED product_id WITH track_id.
            # !!! SHOULD PROBABLY MAKE THIS A FUNCTION TO AVOID CUT AND PASTE ERRORS
            #
            my $trackID = $sale->lic_track_id();

            # Is this a bogus track_id?
            #
            if (! $trackCache{$trackID})
            {
                $self->_report("  skipping - track not in cache", 3);
                RPS::ArtistRoyalty::Process::CreateStatement::logMissedSale($self->{_runID}, $sale, 'Bad lic_track_id');

                my $mi = RPS::DB::Item::SaleRunMap->Create
                (
                    sale_id => $sale->sale_id,
                    run_id => $self->{_runID},
                    run_type => RPS::DB::Item::SaleRunMap::kRunTypeArtistRoyalty,
                    status => RPS::DB::Item::SaleRunMap::kStatusBadProductID,  # !!! WRONG STATUS (we don't have one yet)
                );
                $mi->save();

                next;
            }


            my $trackPayorHash = $trackPayeeMap{$trackID};
            if (! $trackPayorHash)
            {
                $self->_report("...no matching contracts found, skipping sale " . $sale->sale_id, 3);
                RPS::ArtistRoyalty::Process::CreateStatement::logMissedSale($self->{_runID}, $sale, 'No matching contracts', track => $trackCache{$trackID});

                my $mi = RPS::DB::Item::SaleRunMap->Create
                (
                    sale_id => $sale->sale_id,
                    run_id => $self->{_runID},
                    run_type => RPS::DB::Item::SaleRunMap::kRunTypeArtistRoyalty,
                    status => RPS::DB::Item::SaleRunMap::kStatusNoMatchingContracts,
                );
                $mi->save();
            }
            else
            {
                # We have contracts, but do they match our payor id?
                # Not an error if they don't... Just iterate over our payor_ids, and make a sale artist payee map entry for each.
                # !!! Right now there will only be one payor_id... But keep this flexible just in case...
                #
                #
                my %seenPayeeIDs;
                foreach my $payorID (@$allPayorIDs)
                {
                    my $payeeIDHash = $trackPayeeMap{$trackID}{$payorID};
                    if ($payeeIDHash)
                    {
                        foreach my $payeeID (keys %$payeeIDHash)
                        {
                            next if $seenPayeeIDs{$payeeID};
                            $self->_report("creating sale map entry for sale id " . $sale->sale_id . " payeed id $payeeID", 4);
                            my $newMapItem = RPS::DB::Item::SaleArtistPayeeMap->Create
                            (
                                run_id => $self->{_runID},
                                sale_id => $saleID,
                                artist_payee_id => $payeeID,
                            );
                            $newMapItem->save();
                            $seenPayeeIDs{$payeeID} = 1;
                        }
                    }
                }
            }
        }
    }
}


sub _createPayeeStatementJob
{
    my ($self, $payeeID) = @_;
    assert($payeeID);

    # First, create an 'empty' statement database record.
    #
    my $statementObj = RPS::DB::Item::ArtistRoyaltyStatement->Create
    (
        artist_royalty_run_id => $self->{_runID},
        payor_id => $self->{_payorID},
        payee_id => $payeeID,
        status => RPS::Statement::Status::kNotQueued(),
    );
    $statementObj->save();

    my $statementID = $statementObj->artist_royalty_statement_id();


    # Create the Job object
    #
    my $job = RPS::ArtistRoyalty::Job::CreateStatement->new(runID => $self->{_runID}, statementID => $statementID, logLevel => $self->{_logLevel});
    $job->enqueue();


    # Update the statement record with the job id, and
    # update the status to 'in queue'
    #
    $statementObj->job_id($job->id());
    $statementObj->status(RPS::Statement::Status::kInQueue());
    $statementObj->save();

    return $job;
}

sub _addJobToJobList
{
    my ($self, $key, $job) = @_;
    assert($job);

    my $jobID = $job->id;
    die RPS::ArtistRoyalty::Process::Exception->new("no job id!") unless $jobID;
    die RPS::ArtistRoyalty::Process::Exception->new("We already have an entry for job $key") if $self->{_jobs}{$key};

    # So, we're going to use a hash to keep track of our Job objects.
    # Each record in this hash will be a hash as well, so we have some room to
    # jot down any additional state we may want to track.
    # !!! At some point, perhaps, we might use the database for this.
    # !!! I don't really know what we might want to track yet...
    #
    my %jobRecord;
    $jobRecord{job} = $job;

    $self->{_jobs}{$key} = \%jobRecord;
}

sub _getStatement
{
    my ($self, $statementID) = @_;

    my $statement = RPS::DB::Item::ArtistRoyaltyStatement->Lookup(artist_royalty_statement_id => $statementID);
    return $statement;
}


# Return all other committed royalty runs.
# (committed, _NOT_ closed)
#
sub _getAllOtherCommittedRuns
{
    my ($self) = @_;

    my $runID = $self->{_runID};
    return RPS::DB::Item::ArtistRoyaltyRun->GetOtherCommittedRuns($runID);
}


sub _getAllActivePayeeIDs
{
    my ($self) = @_;

    my %idHash;
    my $payorID = $self->{_payorID};
    my $collection = RPS::DB::Item::ArtistPayee->GetAllActiveOrOnHoldForPayor($payorID);
    while (my $payee = $collection->next())
    {
        # Add this payee to the hash
        #
        $idHash{$payee->artist_payee_id} = 1;

    }

    return keys %idHash;
}


sub _preCommitSanityCheck {
    my $self = shift;
    my ($runID) = $self->{_runID};

    # Make sure nobody deleted the pending transactions that appear on this run!
    #
    my $statements = RPS::DB::Item::ArtistRoyaltyStatement->GetByArtistRoyaltyRunID($runID);
    while (my $statement = $statements->next())
    {
        my $statementID = $statement->artist_royalty_statement_id;
        my $statementTransItems = RPS::DB::Item::ArtistRoyaltyTransaction->GetByArtistRoyaltyStatementID($statementID);
        my $transItem;
        while ($transItem = $statementTransItems->next())
        {
            my $pendingTransactionID = $transItem->pending_transaction_id;
            my $pending = RPS::DB::Item::PendingTransaction->Lookup(pending_transaction_id => $pendingTransactionID);
            if (! $pending)
            {
                $self->_report("ERROR - original pending transaction was deleted! : " . Dumper($transItem));
                die RPS::ArtistRoyalty::Process::Exception->new("original pending transaction was deleted", $transItem);
            }
        }

    }

    return 1;
}


sub _getAllStatements
{
    my ($self) = @_;

    my $statements = RPS::DB::Item::ArtistRoyaltyStatement->GetByArtistRoyaltyRunID($self->{_runID});
    return $statements;
}

sub _commitStatement
{
    my ($self, $statement) = @_;

    my $statementID = $statement->artist_royalty_statement_id;
    my $payorID = $statement->payor_id;

    $self->_report("\n------------------------------------\n", 2);

    $self->_report("committing statement id $statementID", 2);


    # Go through each album on the statement.
    #
    my $albums = RPS::DB::Item::ArtistRoyaltyAlbum->GetByArtistRoyaltyStatementID($statementID);
    while (my $album = $albums->next())
    {
        $self->_commitAlbum($album, $statement);
    }


    # Handle the crossed license income account
    #
    if ($statement->cross_collateralized_subtotal < 0)
    {
        # Carry the total balance forward, by applying the subtotal as a transaction.
        #
        if ($statement->contract_level_license_income_subtotal != 0)
        {
            $self->_applyTransactionToContractLevelBalanceAccount($statement->payee_id, $payorID, $statement->contract_level_license_income_subtotal);
        }
    }
    else
    {
        # We paid off any previous balance, so apply a transaction to zero the accuont.
        # !!! If there is an account...
        #
        if ($statement->contract_level_license_income_previous_balance != 0)
        {
            $self->_applyTransactionToContractLevelBalanceAccount($statement->payee_id, $payorID, (-1 * $statement->contract_level_license_income_previous_balance));
        }
    }


    # Now handle the holdover account
    #
    my $total = $statement->total;
    my $artistPayeeID = $statement->payee_id;

    my $payeeAccountMap = RPS::DB::Item::ArtistPayeeAccount->Lookup(artist_payee_id => $artistPayeeID, payor_id => $payorID);
    if (! $payeeAccountMap)
    {
        $payeeAccountMap = RPS::DB::Item::ArtistPayeeAccount->Create
        (
            artist_payee_id => $artistPayeeID,
            payor_id => $payorID,
            # !!! Will need to account for currency_code
        );
        $payeeAccountMap->save();
    }

    my $payeeAccountID = $payeeAccountMap->finance_account_id;
    if (! $payeeAccountID)
    {
        my $account = RPS::DB::Item::FinanceAccount->Create
        (
            description => "advance account for artist payee $artistPayeeID",
            type_code => RPS::DB::Item::FinanceAccount::kAccountTypeAdvance,
            currency_code =>  Common::Client::Current()->Locale()->currencyFormat()->currencyCode(),
        );
        $account->save();
        $payeeAccountID = $account->finance_account_id;
        $self->_report("created new account, id $payeeAccountID", 3);


        # Save the account id in the mapping table.
        #
        $payeeAccountMap->finance_account_id($payeeAccountID);
        $payeeAccountMap->save();
    }


    # Apply the royalties total as an adjustment
    #
    my $statementSubtotal = $statement->total;
    if ($statementSubtotal < 0 or $statementSubtotal > 0)
    {
        $self->_report("adjusting balance by total amount of $statementSubtotal",3);
        my $transaction = RPS::DB::Item::FinanceTransaction->Create
        (
            finance_account_id => $payeeAccountID,
            amount => $statementSubtotal,
            type_code => RPS::DB::Item::FinanceTransaction::kTypeClosingBalance,
            memo => "artist royalty run $self->{_label}",
            # !!! currency_code?
        );
        $transaction->save();
    }

    # Now apply all pending transactions, and delete them.
    #
    my $transactionItems = RPS::DB::Item::ArtistRoyaltyTransaction->GetByArtistRoyaltyStatementID($statementID);
    while (my $item = $transactionItems->next())
    {
        # Fetch the 'real' pending transaction.
        #
        my $pending = RPS::DB::Item::PendingTransaction->Lookup(pending_transaction_id => $item->pending_transaction_id);


        # !!! Note that amount == amount. no sign flipping.  We have to get the sign right when we create the
        # !!! pending transaction in the first place.
        #
        $self->_report("applying pending transaction id " . $pending->pending_transaction_id . " amount of " . $pending->amount(), 3);
        my $transaction = RPS::DB::Item::FinanceTransaction->Create
        (
            finance_account_id => $pending->finance_account_id,
            amount => $pending->amount,
            type_code => $pending->type_code,
            memo => $pending->memo,
            check_number => $pending->check_number,
            currency_code => $pending->currency_code,
            transaction_date => $pending->transaction_date,
        );
        $transaction->save();

        $pending->delete();
    }
}

sub _commitAlbum
{
    my ($self, $album, $statement) = @_;


    my $payorID = $statement->payor_id;

    # Need to adjust this album's balance.
    #
    my $previousBalance = $album->previous_balance;
    my $total = $album->total;
    my $appliedToAccount = 0;


    # Whether we carry a _positive_ balance forward depends on whether
    # this album is crossed, and whether the crossed total is < 0
    #
    if ($album->is_cross_collateralized())
    {
        if ($statement->cross_collateralized_subtotal < 0)
        {
            # Carry forward everything
            #
            # !!! This is WRONG !!!
            # !!! Should be $total - $previousBalance, just like always.
#            $appliedToAccount = $total;
            $appliedToAccount = $total - $previousBalance;
        }
        else
        {
            # Zero out album balances
            #
            $appliedToAccount = (-1 * $previousBalance);
        }
    }
    else
    {
        if ($previousBalance)
        {
            if ($total >= 0)
            {
                $appliedToAccount = (-1 * $previousBalance);
            }
            else
            {
                $appliedToAccount = $total - $previousBalance;
            }
        }
        elsif ($total < 0)
        {
            $appliedToAccount = $total;
        }
    }

    if (0 != $appliedToAccount)
    {

        my $artistPayeeID = $statement->payee_id;
        my $albumID = $album->album_id;
        my $artistContractID = $album->artist_contract_id;

        $self->_report("applying balance of $appliedToAccount to holdover for album $albumID, payee $artistPayeeID", 3);


        # Get the account, post the transaction
        #
        my $accountID;
        my $accountMap = RPS::DB::Item::ArtistRoyaltyAlbumBalanceAccount->Lookup
        (
            album_id => $albumID,
            artist_payee_id => $artistPayeeID,
            payor_id => $payorID,
            artist_contract_id => $artistContractID,
        );
        if ($accountMap)
        {
            $accountID = $accountMap->account_id;
        }
        else
        {
            # Create a new account
            #
            my $account = RPS::DB::Item::FinanceAccount->Create
            (
                description => "holdover account for album $albumID artist payee $artistPayeeID",
                type_code => RPS::DB::Item::FinanceAccount::kAccountTypeHoldover,
                currency_code =>  Common::Client::Current()->Locale()->currencyFormat()->currencyCode(),
            );
            $account->save();
            $accountID = $account->finance_account_id;
            $self->_report("created new holdover account, id $accountID", 3);

            # Create a new mapping
            #
            $accountMap = RPS::DB::Item::ArtistRoyaltyAlbumBalanceAccount->Create
            (
                album_id => $albumID,
                artist_payee_id => $artistPayeeID,
                payor_id => $payorID,
                account_id => $accountID,
                artist_contract_id => $artistContractID,
            # !!! Will need to account for currency_code
            );
            $accountMap->save();
        }


        # Post the transaction
        #
        my $statementID = $statement->artist_royalty_statement_id;
        my $transaction = RPS::DB::Item::FinanceTransaction->Create
        (
            finance_account_id => $accountID,
            amount => $appliedToAccount,
            type_code => RPS::DB::Item::FinanceTransaction::kTypeAdjustment,
            memo => "artist statement $statementID",
            # !!! currency_code?
        );
        $transaction->save();
    }



    # Mark all processed expenses as 'processed'
    #

    my $expenseItems = RPS::DB::Item::ArtistRoyaltyExpenseItem->GetByArtistRoyaltyAlbumID($album->artist_royalty_album_id);
    if ($expenseItems)
    {
        $self->_report("marking " . $expenseItems->size() . " expenses as 'processed'", 3);
        while (my $expenseItem = $expenseItems->next())
        {
            my $expense = RPS::DB::Item::Expense->Lookup(expense_id => $expenseItem->expense_id);
            $expense->processed(1);
            $expense->save();
        }
    }
}

sub _applyTransactionToContractLevelBalanceAccount
{
    my ($self, $payeeID, $payorID, $amount) = @_;

    my $balanceAccount = RPS::DB::Item::ContractLevelLicenseIncomeBalanceAccount->Lookup(artist_payee_id => $payeeID, payor_id => $payorID);
    if (! $balanceAccount)
    {
        $balanceAccount = RPS::DB::Item::ContractLevelLicenseIncomeBalanceAccount->Create(artist_payee_id => $payeeID, payor_id => $payorID);
        $balanceAccount->save();
    }

    my $accountID = $balanceAccount->finance_account_id;
    if (! $accountID)
    {
        my $account = RPS::DB::Item::FinanceAccount->Create
        (
            description => "contract level balance account for artist payee $payeeID",
            type_code => RPS::DB::Item::FinanceAccount::kAccountTypeAdvance,
            currency_code =>  Common::Client::Current()->Locale()->currencyFormat()->currencyCode(),
        );
        $account->save();
        $accountID = $account->finance_account_id;

        # Save the account id in the mapping table.
        #
        $balanceAccount->finance_account_id($accountID);
        $balanceAccount->save();
    }

    my $transaction = RPS::DB::Item::FinanceTransaction->Create
    (
        finance_account_id => $accountID,
        amount => $amount,
        type_code => RPS::DB::Item::FinanceTransaction::kTypeAdjustment,
        memo => "artist royalty run $self->{_label}",
    );
    $transaction->save();
}


sub _setSaleStatePending {
    my $self = shift;
    my $runID = $self->{_runID};
    assert($runID);

    my $saleMapItems = RPS::DB::Item::SaleRunMap->GetPaidByArtistRoyaltyRunID($runID);
    if ($saleMapItems)
    {
        $self->_report("marking " . $saleMapItems->size() . " sales as processed", 3);
        while (my $saleMapItem = $saleMapItems->next())
        {
            my $sale = Raptor::DB::Item::Sale->Lookup(sale_id => $saleMapItem->sale_id);
            $sale->artist_royalty_status(Raptor::DB::Item::Sale::kPending);
            $sale->save();
        }
    }
}

sub _adjustReservesPeriod {
    my $self = shift;
    my $runID = $self->{_runID};
    assert($runID);

    my $reserveMapItems = RPS::DB::Item::ArtistContractTermReserveRun->GetByArtistRoyaltyRunID($runID);
    while (my $reserveMap = $reserveMapItems->next())
    {
        my $reserve = RPS::DB::Item::ArtistContractTermReserve->Lookup
        (
            artist_contract_term_reserve_id => $reserveMap->artist_contract_term_reserve_id,
        );


        my $period = $reserve->periods_remaining();

        if (1 == $period)
        {
            $reserve->liquidated_run_id($runID);
        }

        $reserve->periods_remaining($period - 1);
        $reserve->save();
    }
}


sub _onFailure {
    my $self = shift;
    my %args = @_;
    
    my $status = $args{interupt} ? RPS::RoyaltyRun::Status::kAborted : 
                                   RPS::RoyaltyRun::Status::kError;
    
    my $dbItem = $self->_getRunDBItem();

    if ($args{exception})
    {
        my $e = $args{exception};
        my $errorString;
        if (ref $e && $e->isa('Common::Exception'))
        {
            $errorString = $e->errorMessage();
        }
        else
        {
            $errorString = $e;
        }
        $dbItem->error($errorString);
    }
    
    $dbItem->status($status);
    $dbItem->end_time(Common::DB::Item::kDateTimeNow);
    $dbItem->save();
    
    # Only kill children if a users aborts the process.
    $self->_abortChildren( @_ ) if( $args{interupt} );
    
    $self->SUPER::_onFailure( %args );
}

# Check to ensure our status is "waiting"
sub canExecute {
    my $self = shift;
    my $dbItem = $self->_getRunDBItem();
     
    if( RPS::RoyaltyRun::Status::kWaitingToRun == $dbItem->status() ) {
	return 1;
    } else {
        Common::Log::Print("ERROR - runID " . $self->{_runID} . " is not in the 'waiting' state");
	return;
    }
}


sub _abortChildren {
    my $self = shift;
    my %args = @_;
    
    $self->_report( "  - Marking aborted processes as aborted", 2 );
    my $dbItem = $self->_getRunDBItem();
    my $jobID = $dbItem->job_id;
    
    assert( $jobID );
    
    $self->_report( "  - Found parent job, ID: $jobID", 2 );
    Job::Job->KillChildJobs( $jobID );
}



1;
