#------------------------------------------------------------
# Copyright (C) 2009 RoyaltyShare, Inc.   All Rights Reserved
#------------------------------------------------------------
package RPS::Mechanical::US::Process::RunController;
use strict;

use Data::Dumper;
use File::Path;

use lib '/app/tools/common/lib';
use Common::Assert;
use Common::RSApp;
use Common::Log;
use Common::Timer;


use lib '/app/tools/rps/lib';
use lib '/app/tools/job/lib';
use lib '/app/tools/raptor/lib';

use Raptor::DB::Item::Sale;

use RPS::DB::Item::LicenseReserve;
use RPS::DB::Item::LicenseReserveRun;
use RPS::DB::Item::MechanicalCarryover;
use RPS::DB::Item::MechanicalRun;
use RPS::DB::Item::MechanicalRunCarryover;
use RPS::DB::Item::MechanicalRunTrackLicense;
use RPS::DB::Item::MechanicalStatement;
use RPS::DB::Item::MechanicalStatementItem;
use RPS::DB::Item::MechanicalStatementAdjustmentItem;
use RPS::DB::Item::MechanicalStatementTransaction;
use RPS::DB::Item::MechanicalStatementLicenseTransaction;
use RPS::DB::Item::MechanicalRunTrackLicense;
use RPS::DB::Item::Product;
use RPS::DB::Item::PendingTransaction;
use RPS::DB::Item::ProductTrack;
use RPS::DB::Item::Publisher;
use RPS::DB::Item::SalePublisherMap;
use RPS::DB::Item::SaleRunMap;
use RPS::DB::Item::StatRate;
use RPS::DB::Item::TrackLicense;

use RPS::Statement::Status;
use RPS::Statement::Mechanical::US::PDF;
use RPS::Statement::Mechanical::US::Text;
use Job::Status;
use RPS::Mechanical::US::Process::CreateStatement;
use RPS::Mechanical::US::Job::RunMechanicalRoyalties;
use RPS::Mechanical::US::Job::CreateStatement;
use RPS::Mechanical::US::Job::CreateTextStatements;
use RPS::Mechanical::US::Job::CreatePDFStatements;
use RPS::Mechanical::Process::RunController::Complex;
use RPS::Mechanical::US::Job::CreateReservePipelineReport;
use RPS::DB::Item::ReportQueries::MechanicalReservePipeline;
use RPS::Mechanical::US::Job::CreateRoyaltyExportReport;
use RPS::Mechanical::US::Job::CreateRoyaltyExportReportExcel;

use base 'RPS::Mechanical::Process::RunController::Complex';

use constant kStatementVersion => 4;

sub _runType
{
    return RPS::DB::Item::SaleRunMap::kRunTypeMechanical;
}

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

    # Call the inherited method
    #
    my $rval = $self->SUPER::run();


    # We don't want the data in the SalePublisherMap table to persist.
    # It takes up a lot of space...
    #
    RPS::DB::Item::SalePublisherMap->DeleteAllForRun($self->{_runID});

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

    # Create the royalty data export report
    #
    $self->_createRoyaltyDataExportReport();

    return $rval;
}

sub _trackIsMechanicalExempt
{
    my ($self, $track) = @_;
    return $track->mechanical_exempt();
}

sub _publisherIDFromTrackLicense
{
    my ($self, $tl) = @_;
    return $tl->publisher_id;
}


sub _getAllUnprocessedSales
{
    my ($self, $endDate) = @_;

    my $allSales = Raptor::DB::Item::Sale->GetUnprocessedMechanicalSales(ending_sale_date => $endDate);
    return $allSales;
}

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

    my $allPublishers = RPS::DB::Item::Publisher->GetAll();
    return $allPublishers;
}

sub _publisherIDFromPublisher
{
    my ($self, $publisher) = @_;

    return $publisher->publisher_id();
}


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

    my $allItems = RPS::DB::Item::MechanicalRunTrackLicense->GetByRunIDAndTypeAndCountryCode($self->{_runID}, $self->_runType, 'US');
    return $allItems;
}

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

    my $allItems = RPS::DB::Item::MechanicalRunTrackLicense->DeleteByRunIDAndType($self->{_runID}, $self->_runType);
    return $allItems;
}

sub _getReserveErrors {
    my $self = shift;

    return RPS::DB::Item::TrackLicense->GetAllWithoutReserveLiquidation();
}

sub _getPennyRateErrors {
    my $self = shift;

    return RPS::DB::Item::TrackLicense->GetAllWithInvalidPennyRate();
}

sub _getLicenseShareErrors {
    my $self = shift;

    return RPS::DB::Item::TrackLicense->GetAllWithZeroShare();
}

sub _getAllTrackLicenses
{
    my $self = shift;

    return RPS::DB::Item::TrackLicense->GetAll();
}


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

    my %idHash;
    my $payorID = $self->{_payorID};
    my $collection = RPS::DB::Item::Publisher->GetAllActiveOrOnHoldWithLicensesForPayor($payorID);
    while (my $publisher = $collection->next())
    {
        # Add this publisher to the hash
        #
        $idHash{$publisher->publisher_id} = 1;

        # Also add this guys admin or agent id..
        # agent trumps admin
        #
        if ($publisher->agent_id)
        {
            $idHash{$publisher->agent_id} = 1;
        }
        elsif ($publisher->admin_id)
        {
            $idHash{$publisher->admin_id} = 1;
        }

    }

    return keys %idHash;
}

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

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

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

    $self->_testPendingTransactionsAreValid();

	# See if all the reserves are present and accounted for...
	#
	if (RPS::DB::Item::LicenseReserveRun->CountMissingRunReserves($self->{_runID}) > 0)
	{
		die RPS::Mechanical::Process::Exception->new("ERROR - Not all run reserves are accounted for");
	}

	return 1;
}

sub _logPublicDomainShare
{
    my ($self, $sale, $trackID, $trackLicense, $share) = @_;

    return unless ('US' eq $sale->country_code);
    $self->SUPER::_logPublicDomainShare($sale, $trackID, $trackLicense, $share);
}

sub _nativeCountryCode
{
    my ($self) = @_;
    return 'US';
}


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

    my $runID = $self->{_runID};

    # Make sure nobody deleted the pending transactions that appear on this run!
    #
    my $statements = RPS::DB::Item::MechanicalStatement->GetByMechanicalRunID($runID);
    while (my $statement = $statements->next())
    {
        my $statementID = $statement->mechanical_statement_id;
        my $statementTransItems = RPS::DB::Item::MechanicalStatementTransaction->GetByMechanicalStatementID($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)
            {
                die RPS::Mechanical::Process::Exception->new("original pending transaction was deleted! : ", $transItem);
            }
        }

        my $licenseTransItems = RPS::DB::Item::MechanicalStatementLicenseTransaction->GetByMechanicalStatementID($statementID);
        while ($transItem = $licenseTransItems->next())
        {
            my $pendingTransactionID = $transItem->pending_transaction_id;
            my $pending = RPS::DB::Item::PendingTransaction->Lookup(pending_transaction_id => $pendingTransactionID);
            if (! $pending)
            {
                die RPS::Mechanical::Process::Exception->new("original pending transaction was deleted! : ", $transItem);
            }
        }
    }

    return 1;
}


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


    # What we're really doing here is clearing out the _old_ carryovers.
    # Each 'statement' will then create entries for the new carryover (later).
    #

    # Truncate the carryover table.
    #
    my $runCarryoverIDs = RPS::DB::Item::MechanicalRunCarryover->GetByMechanicalRunID($self->{_runID});
    while (my $carryoverMapItem = $runCarryoverIDs->next())
    {
        my $carryoverID = $carryoverMapItem->mechanical_carryover_id;
        my $carryover = RPS::DB::Item::MechanicalCarryover->Lookup(mechanical_carryover_id => $carryoverID);
        $carryover->delete();
    }


    # Get rid of entries in the carryover run map
    #
# !!! KEEP FOR DEBUGGING
#    RPS::DB::Item::MechanicalRunCarryover->DeleteByMechanicalRunID($self->{_runID});
}


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

    my $runID = $self->{_runID};

    # 'turn the crank' on the reserves table.
    #
    my $runReserves = RPS::DB::Item::LicenseReserveRun->GetByMechanicalRunID($runID);
    while (my $runReserveItem = $runReserves->next())
    {
        my $reserve = RPS::DB::Item::LicenseReserve->Lookup(license_reserve_id => $runReserveItem->license_reserve_id);

		if (! $reserve)
		{
			# This should never happen!
			#
			$self->_report("ERROR!  reserve id " . $runReserveItem->license_reserve_id . " is MISSING");
			next;
		}
        my $period = $reserve->periods_remaining();

        if (1 == $period)
        {
            # This reserve has been liquidated;
            # So we'll make note of the run id.
            #
            $reserve->liquidated_run_id($runID);
        }

        # Just decrement the periods_remaining column.
        # Liquidated reserves will end up with '0' periods_remaining.
        #
        $reserve->periods_remaining($period - 1);
        $reserve->save();
    }

}


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


    # Instantiate the proper CreatePublisherStatement entity, and let that handle the details.
    #
    my $statementProcessor = RPS::Mechanical::US::Process::CreateStatement->new(statementID => $statement->mechanical_statement_id);
    $statementProcessor->commit();
}


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

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


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

    RPS::DB::Item::SaleRunMap->DeleteByMechanicalRunID($self->{_runID});
}

sub _createSalePublisherMapEntry
{
    my ($self, %args) = @_;
    assert($args{run_id});
    assert($args{sale_id});
    assert($args{publisher_id});

    my $newMapItem = RPS::DB::Item::SalePublisherMap->Create(
        run_id => $args{run_id},
        sale_id => $args{sale_id},
        publisher_id => $args{publisher_id},
    );

    $newMapItem->save();

}

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

    my $logBasePath = RPS::Mechanical::US::Job::RunMechanicalRoyalties->LogFilePath(Common::RSApp::GetClientID(), $self->{_runID});
    return $logBasePath;
}


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

    RPS::DB::Item::LicenseReserveRun->DeleteByMechanicalRunID($self->{_runID});
    RPS::DB::Item::MechanicalRunCarryover->DeleteByMechanicalRunID($self->{_runID});

    $self->SUPER::delete();

    # MAKE SURE this is emptied out.
    # We clear this table when a run exits normally, but if something went haywire we might need to try again.
    #
    RPS::DB::Item::SalePublisherMap->DeleteAllForRun($self->{_runID});

	# Clear out any associated pipeline report
	#
	RPS::DB::Item::ReportQueries::MechanicalReservePipeline->DeleteByMechanicalRunID($self->{_runID});

    return 0;
}



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

    my $statementID = $statement->mechanical_statement_id;

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

    $self->_report("  ... deleting reserve items",2);
    RPS::DB::Item::LicenseReserve->DeleteByStatementID($statementID);

    $self->_report("  ... deleting track items",2);
    RPS::DB::Item::MechanicalStatementTrack->DeleteByMechanicalStatementID($statementID);

    $self->_report("  ... deleting license items",2);
    RPS::DB::Item::MechanicalStatementLicense->DeleteByMechanicalStatementID($statementID);

    $self->_report("  ... deleting adjustment items",2);
    RPS::DB::Item::MechanicalStatementAdjustmentItem->DeleteByMechanicalStatementID($statementID);

    $self->_report("  ... deleting statement items",2);
    RPS::DB::Item::MechanicalStatementItem->DeleteByMechanicalStatementID($statementID);

    return 0;
}


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

    # Before we kick off the individual publisher 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->_createSalePublisherMapping();

    $self->SUPER::_createStatementsAndJobs();
}


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

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


sub _createDirectPublisherStatementJob
{
    my ($self, $publisherID) = @_;
    assert($publisherID);

    # First, create an 'empty' statement database record.
    #
    my $statementObj = RPS::DB::Item::MechanicalStatement->Create
    (
        mechanical_run_id => $self->{_runID},
        payor_id => $self->{_payorID},
        publisher_id => $publisherID,
        status => RPS::Statement::Status::kNotQueued(),
        version => kStatementVersion,
    );
    $statementObj->save();

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


    # Create the Job object
    #
    my $job = RPS::Mechanical::US::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 _createPDFJobs
{
    my ($self) = @_;


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

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


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

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


    #my $allStatements = RPS::DB::Item::MechanicalStatement->GetByMechanicalRunID($self->{_runID});
    #while (my $statement = $allStatements->next())
    #{
    #    my $id = $statement->mechanical_statement_id();
    #    my $fileName = RPS::Statement::Mechanical::US::PDF->StatementFileNameFromDBItem($statement);
    #
    #    my $jobArgs = RPS::Mechanical::US::Job::CreatePDFStatement->new
    #    (
    #        statementID => $id,
    #        clientID 	=> $clientID,
    #        filePath 	=> $filePath.$fileName,
    #    );
    #    my $job = $jobArgs->enqueue();
    #}
}

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

    # This report will be stored in the same directory as the mechanical statements.
    #
    my $filePath = RPS::Statement::Mechanical::US::Text->StatementPathFromRunID($self->{_runID});
    if (! -d $filePath)
    {
        mkpath($filePath) or die RPS::Mechanical::Process::Exception->new("ERROR: Unable to create path $filePath: $!");
    }

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

    # Queue the text report
    #
    my $jobArgs = RPS::Mechanical::US::Job::CreateRoyaltyExportReport->new
    (
        runID => $self->{_runID},
        clientID => $clientID,
        filePath => $filePath,
    );
    my $job = $jobArgs->enqueue();

    # Queue the Excel report
    #
    my $jobArgs = RPS::Mechanical::US::Job::CreateRoyaltyExportReportExcel->new
    (
        runID => $self->{_runID},
        clientID => $clientID,
        filePath => $filePath,
    );
    my $job = $jobArgs->enqueue();

    $self->_report("_createRoyaltyDataExportReport - filePath=$filePath", 2);
}


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

    my $filePath = RPS::Statement::Mechanical::US::Text->StatementPathFromRunID($self->{_runID});
    if (! -d $filePath)
    {
        mkpath($filePath) or die RPS::Mechanical::Process::Exception->new("ERROR: Unable to create path $filePath: $!");
    }

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

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


    #my $allStatements = RPS::DB::Item::MechanicalStatement->GetByMechanicalRunID($self->{_runID});
    #while (my $statement = $allStatements->next())
    #{
    #    my $id = $statement->mechanical_statement_id();
    #    my $fileName = RPS::Statement::Mechanical::US::Text->StatementFileNameFromDBItem($statement);
    #    my $jobArgs = RPS::Mechanical::US::Job::CreateTextStatement->new
    #    (
    #        statementID => $id,
    #        clientID 	=> $clientID,
    #        filePath 	=> $filePath.$fileName,
    #    );
    #    my $job = $jobArgs->enqueue();
    #}

}


sub _createUnlicensedTrackLogEntry
{
    my ($self, $album, $label, $track, $product, $trackArtistName, $isrc, $upc, $productType, $missingShare, $totalOwed, $units, $year, $rate) = @_;

    # Skip tracks that are mechanical exempt
    #
    return if ($track->mechanical_exempt);
    
    # Grab the product title from the parent product for digital track products
    my $productTitle;
    
    if ($product->product_type_id == RPS::DB::Item::Product::kProductTypeDigitalTrack && $product->parent_product_id) 
    {
        my $parentProduct = $self->_getProduct($product->parent_product_id);
        $productTitle = $parentProduct->title;
    } 
    else 
    {
        $productTitle = $product->title;
    }    


    my $log = RPS::DB::Item::MechanicalRunUnlicensedTrackLog->Create
    (
	    mechanical_run_id => $self->{_runID},
        label_id => $album->label_id,
        label_name => $label->label_name,
        catalog_number => $album->catalog_number,
	    track_id => $track->track_id,
	    album_id => $album->album_id,
	    album_title => $album->title,
        track_artist_name => $trackArtistName,
	    track_title => $track->title,
	    product_id => $product->product_id,
	    product_title => $productTitle,	    
        isrc => $isrc,
	    upc_ean => $upc,
	    product_type => $productType,
	    unlicensed_share => $missingShare,
	    accrual => $totalOwed,
	    year => $year,
	    rate => $rate,
	    units => $units,
    );
	$self->_report("_createUnlicensedTrackLogEntry: ", 4);
	$self->_report($log, 4);

    return $log;
}

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

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


sub _getStatRates
{
    my ($self) = @_;
    return RPS::DB::Item::StatRate->GetRatesUS();
}

sub _ringtoneStatRateType
{
    my ($self) = @_;
    return RPS::DB::Item::StatRate::kUSRingStatRateType;
}

sub _idFromTrackLicense
{
    my ($self, $trackLicense) = @_;
    return $trackLicense->track_license_id();
}

sub _trackLicenseFromID
{
    my ($self, $trackLicenseID) = @_;

    return RPS::DB::Item::TrackLicense->Lookup(track_license_id => $trackLicenseID);
}

sub _defaultIssueStatRateID { 
    
    my ($self, $licenseType) = @_;
    
    if ($licenseType == RPS::DB::Item::TrackLicense::kRingtone)
    {
        return 28;
    }
    else
    {
        return 16; 
    }
}


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

    $self->_report("_findMatchingLicenses: sale_id = " . $sale->sale_id, 4);

    # !!! This is an important method! :)
    # The license has to be either direct to this publisher, or
    # reported through this publisher.

    # !!! Take region into account here.
    #
    my $matchingRegions = $self->_findRegionsForCountryCode($sale->country_code);
    if (! $matchingRegions || 0 == scalar @$matchingRegions)
    {
        die RPS::Mechanical::Process::SaleException::NoRegion->new($sale, "No matching region found for country code '" . $sale->country_code() . "'");
    }

    my $productID = $sale->product_id;
    my $product = $self->_getProduct($productID);

    if (! $product)
    {
        die RPS::Mechanical::Process::SaleException::NoProduct->new($sale, 
         ($productID ? "Sale had invalid ProductID $productID" : 'Sale had no Product ID!'));
    }

    my $productTypeID = $product->product_type_id;
    my $digitalFlag = $self->_productIsDigital($product);

    my @tracks;
    if (RPS::DB::Item::Product::kProductTypeDigitalTrack ne $productTypeID)
    {
        push @tracks, $self->_getTracksForSale($sale);
    }
    else
    {
        push @tracks, $product->asset_id();
    }


    my $publisherID = $self->_publisherID();
    my $startDate = $sale->date_begin;
    my $endDate = $sale->date_end;
    
    my $ringtoneFlag = 0;
# JPK - Restore when we're ready for RINGBACK
#    if ($sale->format_type eq 'R' || $sale->format_type eq '1')
    if ($sale->format_type eq 'R')
    {
        $ringtoneFlag = 1;
    }


    # Use a nice fat magical accessor - We might as well have the database do some work for us.
    #
    # !!! I don't see how I can filter out licenses that don't match the correct region?
    # !!! Well, maybe I can - Might not be that hard if I can figure out what regions _may_ apply to this sale?
    #
    # I need this to only return licenses that match the product_type_id of the sale!
    # - And they need to be the most 'specific' license : A 'DT' license trumps the 'All Product Type' license.
    #
    #
#    return $self->_getLicensesForPublisherStatementWithTracks(\@tracks, $productTypeID, $matchingRegions, $startDate, $endDate, $publisherID);


    my @trackLicenses;
    foreach my $trackID (@tracks)
    {
		# !!! Skip tracks that are mechanical_exempt
		next if $self->_trackIsExemptFromMechanicals($trackID);

        foreach my $regionID (@$matchingRegions)
        {
            # !!! Note that for a given track and region, there can be multiple matching licenses.
            # That's because this publisher may be an agent/admin : So we need to choose winners for each
            # potential publisher.
            #
            my %winningTrackLicenseHash;

            my $c = $self->_getLicensesForPublisherStatementWithTrackAndRegion($trackID, $regionID, $startDate, $endDate, $publisherID);
            $self->_report("   query for track $trackID region $regionID startDate $startDate endDate $endDate publisherID $publisherID returned " . $c->size() . " licenses", 4);
            while (my $tl = $c->next())
            {
                $self->_report(" looking at track license id " . $self->_idFromTrackLicense($tl), 3);
                $self->_report($tl, 4);

                # Only _1_ track license for a given track and region can go on.
                # But the query may return more than 1, because we have the 'all product' and 'all digital product' types.
                # So I will filter that out here (rather than try and do that in a single query)

                my $licensePublisherID = $self->_publisherIDFromTrackLicense($tl);


                if ($tl->product_type_id == RPS::DB::Item::Product::kProductTypeRingtone && $ringtoneFlag == 1
                 || ! $tl->product_type_id && ($ringtoneFlag == 0)
                 || 255 == $tl->product_type_id && ($digitalFlag == 0 && $ringtoneFlag == 0) # All physical products
                 || 254 == $tl->product_type_id && ($digitalFlag == 1 && $ringtoneFlag == 0)  # All digital products (except ringtones)
                 || $tl->product_type_id == $productTypeID)
                {
                    if ($winningTrackLicenseHash{$licensePublisherID})
                    {
                        # more specific beats less specific.
                        #
                        next if (! $tl->product_type_id);
                        next if (255 != $winningTrackLicenseHash{$licensePublisherID}->product_type_id);
                        next if (254 != $winningTrackLicenseHash{$licensePublisherID}->product_type_id);
                    }

                    $winningTrackLicenseHash{$licensePublisherID} = $tl;
                }
            }

            foreach my $licensePublisherID (keys %winningTrackLicenseHash)
            {
                my $winningTrackLicense = $winningTrackLicenseHash{$licensePublisherID};
                $self->_report(" WINNING TRACK LICENSE ID FOR PUBLISHER $licensePublisherID : " . $self->_idFromTrackLicense($winningTrackLicense), 4);
                push @trackLicenses, $winningTrackLicense;
            }
        }
    }

    return \@trackLicenses;
}

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

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

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

1;
