#!/usr/bin/perl
#------------------------------------------------------------
# Copyright (C) 2007 RoyaltyShare, Inc.   All Rights Reserved
#------------------------------------------------------------
use strict;

use Data::Dumper;
use Getopt::Std;
use POSIX qw(ceil);
use POSIX qw(:sys_wait_h :signal_h :errno_h);
use Sys::Hostname;
use Carp;

use IO::Handle;

use lib '/app/tools/common/lib';
use Common::DB::Item;
use Common::Assert;
use Common::TextProgressBar;
use Common::DB::Item::Client;
use Common::RSMath;

#use lib '/app/tools/data_classes/lib';
#use File::Sale;
use lib '/app/tools/raptor/lib';

#use Raptor::Sale::Sale;
use Raptor::DB::Item::Sale;
use Raptor::DB::Item::File;

use lib '/app/tools/rps/lib';
use RPS::DB::Item::Service;
use RPS::Statement::Mechanical::PDF;
use RPS::Statement::Mechanical::Text;
use RPS::DB::Item::TrackLicense;
use RPS::DB::Item::MechanicalCarryover;
use RPS::DB::Item::MechanicalRunCarryover;
use RPS::DB::Item::Track;
use RPS::DB::Item::Master;
use RPS::DB::Item::Label;
use RPS::DB::Item::Artist;

#use RPS::DB::Item::License;
use RPS::DB::Item::LicenseReserve;
use RPS::DB::Item::LicenseReserveRun;
use RPS::DB::Item::ReserveLiquidation;
use RPS::DB::Item::StatRate;
use RPS::DB::Item::ProductTrack;
use RPS::DB::Item::Song;
use RPS::DB::Item::PendingTransaction;
use RPS::DB::Item::MechanicalRunUnlicensedTrackLog;
use RPS::DB::Item::PublisherTrackCrossedLicenseAccount;
use RPS::DB::Item::MechanicalStatementCrossedLicenseTransaction;

use RPS::DB::Item::Publisher;
use RPS::DB::Item::Product;
use RPS::DB::Item::ProductType;

#use RPS::DB::Item::ControlledComposition;

use RPS::DB::Item::AlbumContract;
use RPS::DB::Item::BonusTrack;
use RPS::DB::Item::MechanicalRun;
use RPS::DB::Item::MechanicalStatement;
use RPS::DB::Item::MechanicalStatementPublisher;
use RPS::DB::Item::MechanicalStatementItem;
use RPS::DB::Item::MechanicalStatementItem;
use RPS::DB::Item::MechanicalStatementTrack;
use RPS::DB::Item::MechanicalStatementLicense;
use RPS::DB::Item::MechanicalStatementAdjustmentItem;
use RPS::DB::Item::SaleMechanicalStatementItemMap;
use RPS::DB::Item::PublisherAdvance;
use RPS::DB::Item::Payor;
use RPS::DB::Item::PublisherAccount;
use RPS::DB::Item::MechanicalStatementLicenseTransaction;

use RPS::DB::Item::SaleRunMap;

use RPS::License::TrackLicense;
use RPS::License::TrackLicenseList;

use RPS::Statement::Mechanical::StatementTransaction;
use RPS::Statement::Mechanical::StatementDisplay;

use RPS::Mechanical::US::Job::CreatePDFStatements;
use RPS::Mechanical::US::Job::CreateTextStatements;

use RPS::RoyaltyRun::Status;
use RPS::XMLCache;

# This constant tunes how often we spit out a 'processed X sales' message
#
use constant kSaleProgressQuanta => 20;

# Caches
#
my $gTrackLicenseReturnsMap;
my $gPublisherStatementMap = {};
my $gPublisherMap;
my $gSongMap;
my $gMasterMap;
my $gTrackMap;
my $gAlbumMap;
my $gLabelMap;
my $gArtistMap;
my $gProductMap;
my $gProductTypeMap;
my $gTrackLicenseMap;
my $gStatRateTable;
my $gStatRateByDate;

my %gPendingTransPubMap;

# Allow tuning of the verbosity of our output.
#
my $gVerbosityLevel = 1;
my $gProgress       = 0;
my $gProfile        = 0;

# Parse the command-line options.
#
my %options;
_parseCommandLine( \%options );

# Instantiate the application singleton object.
#
#my $appSingleton = Common::RSApp->new(clientID => $options{clientID});

my $payorsToRun;

# Get the hostname we are running on.
#
my $hostname = hostname();

my $gRunID = $options{runID};
my $gClient;

# Now, we're going to fork off a child process.
# This way I can tell (and therefore log) whether we exited cleanly or because
# of some exception
#

my $gChildPID = fork();
if ($gChildPID) {
    _parent();
} else {
    _child();
}

sub _child {

    # Install a signal handler to catch ctrl-c
    #
    $SIG{INT} = \&CTRL_C;

    # Instantiate the application singleton object.
    #
    my $appSingleton = Common::RSApp->new( clientID => $options{clientID} );

    # Get the client data
    #
    $gClient = Common::DB::Item::Client->Lookup( client_id => $options{clientID} );

    # Read in the run data.
    #
    my $obj = RPS::DB::Item::MechanicalRun->Lookup( mechanical_run_id => $gRunID );
    if ( !$obj ) {
        die "ERROR: run_id $gRunID is not valid\n";
    }

    # sanity check - make sure this run is in the 'waiting' state
    #
    if ( RPS::RoyaltyRun::Status::kWaitingToRun != $obj->status() ) {
        die "ERROR: run_id $gRunID is not in the 'waiting to run' state\n";
    }

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

    $obj->save();

    my $startDate = $obj->start_date;
    my $endDate   = $obj->end_date;

    my $payorsToRun = [ $obj->payor_id ];

    # We want to know the accrued unpaid balance...
    #
    my $gUnallocatedBalance;

    # Now... do the run!
    #
    _report( "*** Mechanical Royalty Run $gRunID for client id " . $options{clientID} . " for period $startDate - $endDate" );
    _run_mechanicals( $startDate, $endDate, $payorsToRun );

    # Save the accured unpaid balance.
    #
    $obj->unallocated_balance($gUnallocatedBalance);
    $obj->save();

    # As an optimization, let's pre-populate the XMLCache with these statements.
    # That will make it nice and snappy.
    #
    _populateCache( $obj->mechanical_run_id );

    # Go forth and create pdf files, I Command You!
    #
    # Create the new job, and put it in the queue.
    #
    _report("*** adding PDF statements job to queue");
    my $statementsPath = RPS::Statement::Mechanical::PDF::StatementPathFromRunID( $gRunID, $options{clientID} );
    my $jobArgs = RPS::Mechanical::US::Job::CreatePDFStatements->new(
        runID    => $gRunID,
        clientID => $options{clientID},
        filePath => $statementsPath,
    );
    my $job = $jobArgs->enqueue();
    _report("*** done adding PDF statements job to queue!");

    # And finally, let's create some text versions of the statements.
    #
    # Create the new job, and put it in the queue.
    #
    _report("*** adding Text statements job to queue");
    my $statementsPath = RPS::Statement::Mechanical::Text::StatementPathFromRunID( $gRunID, $options{clientID} );
    my $jobArgs = RPS::Mechanical::US::Job::CreateTextStatements->new(
        runID    => $gRunID,
        clientID => $options{clientID},
        filePath => $statementsPath,
    );
    my $job = $jobArgs->enqueue();
    _report("*** done adding Text statements job to queue!");

    # Mark the run as complete
    #
    #    $obj->status(RPS::RoyaltyRun::Status::kComplete);
    #    $obj->end_time(Common::DB::Item::kDateTimeNow);
    #    $obj->save();
}

my $gChildExitCode;
my $gChildIsRunning;

sub _parent {
    local $SIG{CHLD} = \&REAPER;

    local $SIG{INT} = 'IGNORE';

    $gChildIsRunning = 1;
    while ($gChildIsRunning) {
        sleep(1);
    }

    my $status;
    if ( 0 == $gChildExitCode ) {
        $status = RPS::RoyaltyRun::Status::kComplete;
    } elsif ( 1 == $gChildExitCode ) {
        $status = RPS::RoyaltyRun::Status::kAborted;
    } else {
        $status = RPS::RoyaltyRun::Status::kError;
    }

    my $appSingleton = Common::RSApp->new( clientID => $options{clientID} );
    my $obj = RPS::DB::Item::MechanicalRun->Lookup( mechanical_run_id => $gRunID );
    $obj->status($status);
    $obj->end_time(Common::DB::Item::kDateTimeNow);
    $obj->save();

    exit(0);
}

sub REAPER {
    my $deadChildPID = waitpid( -1, &WNOHANG );

    if ( -1 == $deadChildPID ) {

        # no child - ignore this
    } elsif ( WIFEXITED($?) ) {

        # The child really exited.
        # Let's find out why.
        #
        $gChildExitCode  = $? >> 8;
        $gChildIsRunning = 0;
    }
    $SIG{CHLD} = \&REAPER;
}

sub CTRL_C {
    _report("caught a ctrl-c, aborting");
    exit(1);
}

sub _run_mechanicals {
    my ( $startDate, $endDate, $payorIDs ) = @_;

    # Transform the payorIDs array into an identity hash.
    #
    my %payorIDMap;
    foreach my $payorID (@$payorIDs) {
        $payorIDMap{$payorID} = 1;
    }

    my %adjustmentState;
    my %controlledCompState;

    #
    # ---  Discover new ccomp-related licenses.
    #
    _report("--- finding new licenses");
    my $ccompTrackLicensesToAdjust = _findLicensesToAdjust( $startDate, $endDate, \%payorIDMap );

    #    _report(Dumper($ccompTrackLicensesToAdjust), 4);

    #
    # ---  Process Sales
    #
    _report("--- calculating license royalties");
    _processSales( $ccompTrackLicensesToAdjust, \%adjustmentState, \%controlledCompState, $startDate, $endDate, \%payorIDMap );

    _report( "adjustment state: " . Dumper( \%adjustmentState ),          4 );
    _report( "controlled comp state: " . Dumper( \%controlledCompState ), 4 );

    #
    # ---  Pay royalties on Controlled Composition Clauses
    #
    _report("--- calculating controlled composition royalties");
    _processControlledComp( \%controlledCompState, $startDate, $endDate );

    #
    # ---  Process 'adjustments' on Controlled Comps
    #
    # skip this for Dualtone right now...
    if ( $options{clientID} == 14 ) {
        _report("--- skipping controlled composition adjustments");
    } else {
        _report("--- calculating controlled composition adjustments");
        _processControlledCompAdjustments( \%adjustmentState, $startDate, $endDate );
    }

    #
    # ---  Figure in License Advances and calculate per-track subtotals
    #
    _report("--- calculating advances, aggregating subtotals by song");
    _finalizeStatements( $startDate, $endDate );

    #
    # ---  Flag the statements as 'pending', and calculate total owed.
    #
    _report("--- marking statements as pending");
    _markStatementsAsPending();

}

# Pass dates in 'yyyy-mm-dd' format.
#
sub _parseCommandLine {
    my ($settings) = @_;

    my %opt;
    getopts( 'r:c:V:', \%opt );

    if ( !$opt{r} || !$opt{c} ) {
        _usage();
        exit(1);
    }
    $settings->{runID}    = $opt{r};
    $settings->{clientID} = $opt{c};

    if ( defined $opt{V} ) {
        $gVerbosityLevel = $opt{V};
    }
}

sub _usage {
    print "\nusage: $0 -c client_id -s start_date -e end_date [-V n]\n";
    print "\n";
    print "Arguments:\n";
    print "\t-c <client_id>\t\tThe client_id of the client to process\n";
    print "\t-r <run_id>\t\tThe id of the mechanical run to process\n";
    print "\t-V <N>\t\t\tVerbosity level - 0 means no output\n";
}

sub _processSales {
    my ( $adjustedTrackLicenses, $adjustmentState, $controlledCompState, $startDate, $endDate, $payorIDMap ) = @_;

    my $sales = Raptor::DB::Item::Sale->GetDualtoneUnprocessedMechanicalSales(
        endDate => $endDate,

        #        countryCode => 'US',
        skipFree => 1,
    );

    my $progressBar = Common::TextProgressBar->new( $sales->size, kSaleProgressQuanta );
    if ($gProgress) {
        $| = 1;
        $progressBar->display( \*STDOUT );
    }

    # I want to speed up the product-track matching.
    # I figure we'll be asking for the same product ids multiple times, so why not
    # cache that here?
    # (But, I don't want to cache this globally - I'm using too much ram as it is...)
    #
    my %productTrackCache;

    my %productUnitMap;
    my %overshareMap;
    my %missingShareMap;

    _report( "NUMBER OF SALES: " . $sales->size, 2 );

    while ( my $sale = $sales->next() ) {

        # !!! Why do we need both?
        # !!! Doesn't it make more sense to simply use the _applicable_ stat rate id?
        # !!! A reasonably major change, but a worth-while one, I'd wager.
        #
        # !!! In the short term, though, I can find everyplace where I refer to issueStatRateID
        # !!! and set it to 0.  At this moment, I know that we _never_ actually use issueStatRateID anyway.
        # !!! I can alter the tables later to convert them to applied_stat_rate_id

        # We'll need to refer to both the stat rate that applied at the time the product
        # was released, and the stat rate that applied at the time of the sale.
        #
        # Alas, we don't actually have a firm date for these sales - All we have is
        # a date range, which corresponds to the reporting period from which these
        # sales were aggregated.   This means that there is the possibility that
        # the units in this sales record fall on opposite sides of a stat rate bump!
        # At this point, there is nothing we can do to get the 'real' date of sale, so
        # it would seem the safest thing to do is err on the side of the later date.
        #
        my $saleStatRateID = _getStatRateID( $sale->date_end );

        # !!! Skipping these for now.
        #
        if ( !$saleStatRateID ) {
            my $mi = RPS::DB::Item::SaleRunMap->Create(
                sale_id  => $sale->sale_id,
                run_id   => $gRunID,
                run_type => RPS::DB::Item::SaleRunMap::kRunTypeMechanical,
                status   => RPS::DB::Item::SaleRunMap::kStatusNoStatRate,
            );
            $mi->save();

            next;
        }

        assert( $saleStatRateID, "ERROR - can't match stat rate for sale: " . Dumper($sale) );

        my $productID = $sale->product_id;
        if ( !$productID ) {
            _report( " !!! skipping sale - missing product id: " . Dumper($sale) );
            my $mi = RPS::DB::Item::SaleRunMap->Create(
                sale_id  => $sale->sale_id,
                run_id   => $gRunID,
                run_type => RPS::DB::Item::SaleRunMap::kRunTypeMechanical,
                status   => RPS::DB::Item::SaleRunMap::kStatusBadProductID,
            );
            $mi->save();
            next;
        }

        # (XXX) Optimize product lookups
        #
        my $product = _getProduct($productID);

        if ( !$product ) {
            _report( " !!! skipping sale - invalid product id: " . Dumper($sale) );
            my $mi = RPS::DB::Item::SaleRunMap->Create(
                sale_id  => $sale->sale_id,
                run_id   => $gRunID,
                run_type => RPS::DB::Item::SaleRunMap::kRunTypeMechanical,
                status   => RPS::DB::Item::SaleRunMap::kStatusBadProductID,
            );
            $mi->save();
            next;
        }

        # We want to skip sales that refer to 'inactive' albums.
        #
        if ( _productAlbumIsInactive($product) ) {
            _report(" !!! skipping sale - album is inactive");
            my $mi = RPS::DB::Item::SaleRunMap->Create(
                sale_id  => $sale->sale_id,
                run_id   => $gRunID,
                run_type => RPS::DB::Item::SaleRunMap::kRunTypeMechanical,
                status   => RPS::DB::Item::SaleRunMap::kStatusInactiveAlbum,
            );
            $mi->save();
            next;
        }

        # (XXX) Product type id is nice and all, but what I am probably going to want
        #  is to know whether this is digital or not, so I can find the correct license.
        #
        my $productTypeID = $product->product_type_id;

        # !!! This is a bit of a hack - if we don't have a release date, just
        # use the date_end.
        #
        my $releaseDate = $product->release_date;
        $releaseDate = $sale->date_end unless $releaseDate;

        #        my $issueStatRateID  = _getStatRateID($releaseDate);
        my $issueStatRateID = 16;

        # Build a data structure that allows us to determine whether
        # we've already paid mechanicals for a given track_license,
        # and what the %share was.
        # This will enable us to detect when a new license has been
        # added, and we therefore need to re-visit a sale.
        # -- !!! How about controlled comp?  If we discover a new
        # -- license has been added, we will need to re-calculate what
        #    is owed to the controlled comp...
        #
        # -- Primary key will be track_license_id
        #
        my $saleID = $sale->sale_id;
        my $previousSaleStatementItems = _getPreviousSaleStatementItems( $saleID, $issueStatRateID, $saleStatRateID );
        _report( "\n\n-----------------------------------------------\nsale: " . Dumper($sale), 4 );
        _report( "previous sale statement items: " . Dumper($previousSaleStatementItems),       4 );

        # Dereference the number of units in this sale.
        #
        # jpk - Now it gets complicated...
        # For _digital_ sales, the units are the thing.
        # For _physical_ sales, we have the Sales column and the Returns column.
        #  and units == sales - returns.
        #  But, we do want to track all this for everybody. whee!
        #
        my $units      = $sale->units;
        my $numSales   = $sale->sales;
        my $numReturns = $sale->returns;

        if ( _productIsDigital($productID) ) {
            if ( $units < 0 ) {
                $numReturns = ( -1 * $units );
            } else {
                $numSales = $units;
            }
        } else {
            $units = $numSales - $numReturns;
            _report( "---physical sale: units = $units", 3 );
        }

        # Fetch all the tracks associated with this sale.
        # jpk - going to update this accessor function to skip tracks that
        # do not have a valid duration.
        #
        if ( !$productTrackCache{ $product->product_id } ) {
            $productTrackCache{ $product->product_id } = _getTracksFromProduct($product);
        }
        my $productTracks = $productTrackCache{ $product->product_id };

        _report( "  track count: " . scalar @$productTracks, 3 );

        # If there aren't any tracks, skip this sale.
        # (I don't think I want to mark this sale as pending in this case.)
        #
        if ( 0 == scalar @$productTracks ) {
            _report( "  no product tracks, skipping", 3 );
            my $mi = RPS::DB::Item::SaleRunMap->Create(
                sale_id  => $sale->sale_id,
                run_id   => $gRunID,
                run_type => RPS::DB::Item::SaleRunMap::kRunTypeMechanical,
                status   => RPS::DB::Item::SaleRunMap::kStatusNoProductTracks,
            );
            $mi->save();
            next;
        }

        # Determine the service id, if possible.
        #
        my $serviceID = _fileIDToService( $sale->file_id );

        my $paidLicenses;
        foreach my $productTrack (@$productTracks) {
            my $trackID = $productTrack->track_id;

            # Get the track data.
            #
            my $track = _getTrack( $productTrack->track_id );

            # !!! When do I take bonus tracks into account?
            # !!! Perhaps... right here?
            #
            if ( RPS::DB::Item::ProductTrack->TrackIsBonusTrack($trackID) ) {
                _report( "  track $trackID is a bonus track", 3 );

                # Does this bonus track appear on this product for this service?
                #
                my $bonusTrackEntry = RPS::DB::Item::BonusTrack->Lookup(
                    track_id   => $trackID,
                    product_id => $productID,
                    service_id => $serviceID,
                );

                if ( !$bonusTrackEntry ) {
                    _report( "    not a bonus track for service $serviceID, skipping", 3 );
                    next;
                }
            }

            # get the appropriate track-licenses for this track.
            # !!! So, payor ids... I only want to generate statements for the payor ids I was given.
            # !!! HOWEVER, I don't want to screw up controlled comp calculations by skipping
            # !!! licenses that don't belong to that set of payors.
            # !!! This seems to mean, then, that I still have to get every track license...  payor_id
            # !!! will have to be considered later.
            #
            # !!! Use the start and end date from the _sale_.
            #
            my $trackLicenseList =
              _getTrackLicenses( $track->track_id(), $sale->date_begin, $sale->date_end, $productTypeID, $sale->country_code );

            #            my $trackLicenseList = _getTrackLicenses($track->track_id(), $startDate, $endDate, $productTypeID);

            # Keep track of the share amount - if this exceeds 100%, that's a problem and we will
            # want to report that (and not pay on those licenses)
            # jpk - going to have to do this in a seperate loop.  It's a lot simpler than trying
            # to remove all the stuff I've added to the productUnitMap after the fact...
            #
            my $shareSum = 0;
            my @licenseIDList;
            _report( "   num track licenses : " . scalar @$trackLicenseList, 4 );
            foreach my $trackLicense (@$trackLicenseList) {
                my $trackLicenseID = $trackLicense->track_license_id;
                push @licenseIDList, $trackLicenseID;
                my $share = $trackLicense->share;
                $shareSum += $share;
            }

            # !!! May want to accumulate this and output it all at once...
            # !!! Otherwise we'll get a lot of repeated lines.
            #
            _report( "   shareSum = $shareSum", 4 );
            if ( $shareSum > 100 ) {
                my $printableList = join( ",", @licenseIDList );
                $overshareMap{$productID}{$trackID}{list}  = $printableList;
                $overshareMap{$productID}{$trackID}{share} = $shareSum;

                # !!! We're going to go ahead and pay on these now.
                #
                # !!! Except for select client ids.
                #
                my $clientID = Common::RSApp::GetClientID();
                if (
                    31 == $clientID       # sanctuary
                    || 47 == $clientID    # madacy
                  ) {
                    _report( "    skipping sale - we don't pay mechanicals for client $clientID", 3 );
                    my $mi = RPS::DB::Item::SaleRunMap->Create(
                        sale_id  => $sale->sale_id,
                        run_id   => $gRunID,
                        run_type => RPS::DB::Item::SaleRunMap::kRunTypeMechanical,
                        status   => RPS::DB::Item::SaleRunMap::kStatusClientDoesntPayOvershareMechanicals,
                    );
                    $mi->save();
                    next;
                }

                #                next;
            }

            # !!! They want an 'undershare' report, too.
            #
            #  Only including US sales for now.
            #
            if ( ( $shareSum < 99 ) && ( $sale->country_code eq 'US' ) ) {
                my $missingShare = 100 - $shareSum;

#_report("    adding entry to missing share map - product=".$productID.",track=".$trackID.",share=".$missingShare.",sale_country=".$sale->country_code);
                $missingShareMap{$trackID}{$productID}{$saleStatRateID}{share} = $missingShare;
                $missingShareMap{$trackID}{$productID}{$saleStatRateID}{units} += $units;
            }

            # !!! jpk - Can I skip track licenses that don't have the requested payor id _here_?
            #
            foreach my $trackLicense (@$trackLicenseList) {
                my $trackLicenseID = $trackLicense->track_license_id;
                _report( "  +++ processing sale with track license $trackLicenseID ", 3 );

                # Have we already paid out this track license in a previous lifetime?
                # If so, we may need to make adjustments to the controlled comp account.
                #
                # In other words, if we've added a new license since the last time we
                # processed this sale, and there was some $ given to the unknown publisher,
                # we are probably going to owe the ccomp publisher some money...
                #
                if ( $previousSaleStatementItems->{$trackLicenseID} ) {
                    my $previousStatementItem = $previousSaleStatementItems->{$trackLicenseID};

                    _report( "   -- detected a previous sale - looking for adjusted license with id $trackLicenseID", 3 );

                    if ( $adjustedTrackLicenses->{$trackLicenseID} ) {

                        # This statement item will need adjusting.
                        # !!! Sigh... This is going to be a wee bit irritating. Maybe I can just
                        # handle this inline, right here?
                        #
                        _report( "   ++ found adjusted license, adding to adjustment state", 3 );
                        push @{ $adjustmentState->{$trackLicenseID} }, $previousStatementItem;
                    }

                    # Skip this license
                    # After all, we've already _paid_ this license holder for this sale.
                    # The only point to this exercise is to gather some state to allow us
                    # to calculate adjustments later...
                    #
                    _report( "    skipping this!  already paid", 3 );
                    next;

                }

                # Skip the license if it isn't valid.
                #
                if ( !_licenseIsValid($trackLicense) ) {
                    next;
                }

                if ( !_productIsDigital($productID) ) {
                    _report(
"loop 1 : looking at a physical product!  productID $productID units $units numSales $numSales numReturns $numReturns",
                        2
                    );
                }

                # If the license is a penny rate, then we don't want to sort sales by stat rate id -
                # the stat rate id is irrelevant, and we want all sales, returns, etc. to settle into
                # a single bucket.
                # We'll do that by forcing the ids to '0'.
                # !!! Need to remember to do this same thing when dealing with carryover, liquidations, etc.
                #
                my $tempIssueStatRateID = $issueStatRateID;
                my $tempSaleStatRateID  = $saleStatRateID;
                if ( RPS::DB::Item::TrackLicense::kRateTypePenny == $trackLicense->rate_type ) {
                    _report( "+++ sale TrackLicense has a penny rate, setting rate ids to 0: " . Dumper($trackLicense), 3 );

                    #                    $issueStatRateID = $saleStatRateID = 0;
                    $tempIssueStatRateID = $tempSaleStatRateID = 0;
                }

                # !!! Ok, here is where I can insert my refactoring.
                # Accumulate all the unit totals from the sales table.
                #
                #                $productUnitMap{$trackLicenseID}{$productID}{$issueStatRateID}{$saleStatRateID}{units} += $units;

                $productUnitMap{$trackLicenseID}{$productID}{$tempIssueStatRateID}{$tempSaleStatRateID}{sales}   += $numSales;
                $productUnitMap{$trackLicenseID}{$productID}{$tempIssueStatRateID}{$tempSaleStatRateID}{returns} += $numReturns;
                push @{ $productUnitMap{$trackLicenseID}{$productID}{$tempIssueStatRateID}{$tempSaleStatRateID}{saleIDs} }, $sale->sale_id;

                $paidLicenses++;
            }
        }

        if ( !$paidLicenses ) {

            # We want to report that we looked at this sale, but couldn't find any new licenses
            # to pay on.
            #
            my $mi = RPS::DB::Item::SaleRunMap->Create(
                sale_id  => $sale->sale_id,
                run_id   => $gRunID,
                run_type => RPS::DB::Item::SaleRunMap::kRunTypeMechanical,
                status   => RPS::DB::Item::SaleRunMap::kStatusNoLicense,
            );
            $mi->save();
        }

        # Flag the sale as 'pending' - This will allow us to roll back later if necessary.
        #
        # !!! Really, why do I do this here?
        # !!! Seems like we ought not to bother setting any status codes until we commit.
        # !!! Not to mention that this sort of 'pending' status is really not that useful.
        $sale->mechanical_royalty_status(Raptor::DB::Item::Sale::kPending);
        $sale->save();

        $progressBar->increment();
        if ($gProgress) {
            $| = 1;
            $progressBar->display( \*STDOUT );
        }
    }

    _report( " 1) productUnitMap : " . Dumper(%productUnitMap), 4 );

    # Now, run through the LicenseReserve table.
    # There may be units waiting to be liquidated that do not correlate with any sales.
    # --- skip reserves that don't map to our payorIDMap
    #
    my $reserves = RPS::DB::Item::LicenseReserve->GetAllCommitted();
    while ( my $reserve = $reserves->next() ) {
        my $trackLicenseID = $reserve->track_license_id;

        my $productID = $reserve->product_id;

        #        my $issueStatRateID = $reserve->issue_stat_rate_id;
        my $issueStatRateID = 16;
        my $saleStatRateID  = $reserve->sale_stat_rate_id;

        #        assert($issueStatRateID);
        #        assert($saleStatRateID);

        # Now we iterate over the reserve list, adding units that need to be liquidated.
        # !!! Note - I'm changing this around now - We won't be saving any changes to the
        # liquidation table in this script (except to add new ones).  Adjusting the table
        # will happen in the 'commit' script.
        #
        my $totalUnits = 0;
        my $period     = $reserve->periods_remaining;

        # !!! So, here's a point where we fetch the track license.
        # Would it be possible to just not do this at all?
        # What breaks if I comment this line out?
        #
        my $trackLicense = RPS::DB::Item::TrackLicense->Lookup( track_license_id => $trackLicenseID );

        # Skip the license if it isn't valid.
        #
        if ( !_licenseIsValid($trackLicense) ) {
            next;
        }

        # Skip this reserve if it doesn't belong to the payors we are running.
        #
        if ( !$payorIDMap->{ $trackLicense->payor_id } ) {
            _report( "skipping reserve " . $reserve->license_reserve_id . " - not running that payor", 3 );
            next;
        }

        # If this track license is inactive, or the _publisher_ who owns it is inactive, then skip this reserve.
        # OR if the album the track is found on is inactive...
        #
        if ( $trackLicense->inactive() ) {
            _report( "skipping reserve " . $reserve->license_reserve_id . " - track license is inactive", 3 );
            next;
        }

        my $pubData = _getPublisher( $trackLicense->publisher_id );
        if ( $pubData && RPS::DB::Item::Publisher::kStatusInactive == $pubData->status() ) {
            _report( "skipping reserve " . $reserve->license_reserve_id . " - publisher is inactive", 3 );
            next;
        }

        my $track = RPS::DB::Item::Track->Lookup( track_id => $trackLicense->track_id );
        my $album = RPS::DB::Item::Album->Lookup( album_id => $track->album_id );
        if ( $album->inactive() || $album->status() == 0 ) {
            _report( "skipping reserve " . $reserve->license_reserve_id . " - album is inactive", 3 );
            next;
        }

        # If the license is a penny rate, then we don't want to sort sales by stat rate id -
        # the stat rate id is irrelevant, and we want all sales, returns, etc. to settle into
        # a single bucket.
        # We'll do that by forcing the ids to '0'.
        # !!! Need to remember to do this same thing when dealing with carryover, liquidations, etc.
        #
        if ( RPS::DB::Item::TrackLicense::kRateTypePenny == $trackLicense->rate_type ) {
            _report( "+++ reserve TrackLicense has a penny rate, setting rate ids to 0: " . Dumper($trackLicense), 3 );
            $issueStatRateID = $saleStatRateID = 0;
        }

        # If period == 1, then we're liquidating now.
        #
        if ( 1 == $period ) {

            # Determine the number of reserved units to liquidate.
            #
            my $units = $reserve->units;

            $productUnitMap{$trackLicenseID}{$productID}{$issueStatRateID}{$saleStatRateID}{liquidated} += $units;
        }

        # Associate this reserve with this run, so we can increment it's periods_remaining someday.
        #
        my $runMapItem = RPS::DB::Item::LicenseReserveRun->Create(
            license_reserve_id => $reserve->license_reserve_id,
            mechanical_run_id  => $gRunID,
        );
        $runMapItem->save();
    }

    # Do vitually the same thing with carryover units.
    # However, there isn't a fancy liquidation schedule - We look at
    # all carryovers every period.
    #
    my $carryovers = RPS::DB::Item::MechanicalCarryover->GetAll();
    while ( my $carryover = $carryovers->next() ) {
        my $trackLicenseID = $carryover->track_license_id;

        my $carryoverTrackLicense = RPS::DB::Item::TrackLicense->Lookup( track_license_id => $trackLicenseID );
        my $carryoverPub = _getPublisher( $carryoverTrackLicense->publisher_id );
        if ( $carryoverPub && RPS::DB::Item::Publisher::kStatusInactive == $carryoverPub->status() ) {
            _report( "skipping carryover " . $carryover->mechanical_carryover_id . " : publisher is inactive", 2 );
            next;
        }

        # Skip this carryover if it doesn't belong to the payors we are running.
        #
        if ( !$payorIDMap->{ $carryoverTrackLicense->payor_id } ) {
            _report( "skipping carryover" . $carryover->mechanical_carryover_id . " - not running that payor", 2 );
            next;
        }

        # If this track license is inactive, or the _publisher_ who owns it is inactive, then skip this reserve.
        #
        if ( $carryoverTrackLicense->inactive() ) {
            _report( "skipping carryover" . $carryover->mechanical_carryover_id . " - track license is inactive", 2 );
            next;
        }

        my $track = RPS::DB::Item::Track->Lookup( track_id => $carryoverTrackLicense->track_id );
        my $album = RPS::DB::Item::Album->Lookup( album_id => $track->album_id );
        if ( $album->inactive() || $album->status() == 0 ) {
            _report( "skipping carryover " . $carryover->mechanical_carryover_id . " - album is inactive", 3 );
            next;
        }

        # Skip the license if it isn't valid.
        #
        if ( !_licenseIsValid($carryoverTrackLicense) ) {
            next;
        }

        # We're going to liquidate this carryover this run, so make a note of that.
        #
        my $mechanicalRunCarryoverItem = RPS::DB::Item::MechanicalRunCarryover->Create(
            mechanical_carryover_id => $carryover->mechanical_carryover_id,
            mechanical_run_id       => $gRunID,
        );
        $mechanicalRunCarryoverItem->save();

        my $productID = $carryover->product_id;

        #        my $issueStatRateID = $carryover->issue_stat_rate_id;
        my $issueStatRateID = 16;
        my $saleStatRateID  = $carryover->sale_stat_rate_id;
        my $units           = $carryover->units;

        # If the license is a penny rate, then we don't want to sort sales by stat rate id -
        # the stat rate id is irrelevant, and we want all sales, returns, etc. to settle into
        # a single bucket.
        # We'll do that by forcing the ids to '0'.
        # !!! Need to remember to do this same thing when dealing with carryover, liquidations, etc.
        #
        if ( RPS::DB::Item::TrackLicense::kRateTypePenny == $carryoverTrackLicense->rate_type ) {
            _report( "+++ carryoverTrackLicense has a penny rate, setting rate ids to 0: " . Dumper($carryoverTrackLicense), 3 );
            $issueStatRateID = $saleStatRateID = 0;
        }

        # We could now have multiple carryover entries that hash to the same slot.
        # Hence the +=...
        #
        $productUnitMap{$trackLicenseID}{$productID}{$issueStatRateID}{$saleStatRateID}{carryover} += $units;
    }

    # (XXX) I think this is the correct place to deal with accrued units.
    #       Replaces the _getReturns call.
    #       Instead, I will need to organize the productUnitMap by publisher_id/track_id
    #       Then find all the accrued units

    $progressBar = Common::TextProgressBar->new( scalar keys %productUnitMap, kSaleProgressQuanta );
    if ($gProgress) {
        $| = 1;
        $progressBar->display( \*STDOUT );
    }

    # Now, we have all the units, sales, and returns (as told to us by the sales table) accumulated.
    # This means that we're going to now be assured of visiting each potential statement item once.
    #
    _report( "productUnitMap: " . Dumper( \%productUnitMap ), 2 );
    foreach my $trackLicenseID ( keys %productUnitMap ) {
        my $trackLicense = RPS::DB::Item::TrackLicense->Lookup( track_license_id => $trackLicenseID );

        # Are we really dealing with this license's payor this time around?
        #
        my $payorID = $trackLicense->payor_id;
        if ( !$payorIDMap->{$payorID} ) {
            _report( "   -- skipping track_license $trackLicenseID - its payor_id of $payorID is not on our list", 2 );
            next;
        }

        # If the track license is inactive, skip it
        #
        if ( $trackLicense->inactive() ) {
            _report( "   -- skipping carryover for track_license $trackLicenseID - inactive", 2 );
            next;
        }

        my $productHash = $productUnitMap{$trackLicenseID};
        foreach my $productID ( keys %$productHash ) {
            my $product = _getProduct($productID);
            if ( !_productIsDigital($productID) ) {
                _report( "loop 2 : looking at a physical product!  productID $productID", 2 );
            }
            my $issueStatRateHash = $productHash->{$productID};
            foreach my $issueStatRateID ( keys %$issueStatRateHash ) {
                my $saleStatRateHash = $issueStatRateHash->{$issueStatRateID};
                foreach my $saleStatRateID ( keys %$saleStatRateHash ) {
                    my $record            = $productUnitMap{$trackLicenseID}{$productID}{$issueStatRateID}{$saleStatRateID};
                    my $numSales          = $record->{sales};
                    my $numReturns        = $record->{returns};
                    my $saleIDs           = $record->{saleIDs};
                    my $liquidated        = $record->{liquidated};
                    my $previousCarryover = $record->{carryover};

                    my $trackID = $trackLicense->track_id;
                    my $track   = _getTrack($trackID);

                    # We'll need the track duration for stat rate calulcations.
                    #
                    my $masterID = $track->master_id;
                    die "!!! Track has no master, cannot determine duration" unless $masterID;

                    my $masterData = _getMaster($masterID);
                    my $duration   = $masterData->duration;

                    _report( "   trackLicenseID: $trackLicenseID", 3 );

                    # Skip this license if it's the controlled composition clause.
                    # (But first, we'll stash the controlled comp away for later perusal)
                    #
                    if ( RPS::DB::Item::TrackLicense::kControlledComposition eq $trackLicense->type ) {
                        my $albumID = $track->album_id;
                        if ($albumID) {

                            # !!!
                            # We are going to need the issue stat rate later.
                            # We don't store this in the controlled comp table.
                            # In theory, I ought to be able to use the value we have right
                            # now - If our data is good, then the release date for all the
                            # tracks on a given album should be the same....
                            #
                            my $ccID = _getControlledCompIDFromAlbumID($albumID);
                            if ( !$ccID ) {
                                _report(
"!!! ERROR !!! trackLicense $trackLicenseID is type controlled composition, but controlled_composition_id is invalid!  SKIPPING"
                                );
                                next;
                            }

                            my $share = $trackLicense->share;
                            _report( "    license is controlled comp - share = $share", 3 );

                            my %newRecord;
                            $controlledCompState->{$albumID}{$ccID}{$trackLicenseID}{$productID}{$issueStatRateID}{$saleStatRateID}
                              {numSales} = $numSales;
                            $controlledCompState->{$albumID}{$ccID}{$trackLicenseID}{$productID}{$issueStatRateID}{$saleStatRateID}
                              {numReturns} = $numReturns;
                            $controlledCompState->{$albumID}{$ccID}{$trackLicenseID}{$productID}{$issueStatRateID}{$saleStatRateID}{saleIDs}
                              = $saleIDs;
                            $controlledCompState->{$albumID}{$ccID}{$trackLicenseID}{$productID}{$issueStatRateID}{$saleStatRateID}
                              {liquidated} = $liquidated;
                            $controlledCompState->{$albumID}{$ccID}{$trackLicenseID}{$productID}{$issueStatRateID}{$saleStatRateID}
                              {carryover} = $previousCarryover;

                        } else {
                            croak "!!! We have a controlled comp on a track with no album?";
                        }
                        next;
                    }

                    # If this is a public domain share, make a note of the share, then skip.
                    #
                    if ( RPS::DB::Item::TrackLicense::kPublicDomain eq $trackLicense->type ) {
                        my $share = $trackLicense->share;
                        _report( "    license is public domain - share = $share", 3 );

                        next;
                    }

                    my $publisherID = _getCorrectPublisherID($trackLicense);

                    _report( "   calculating royalties for trackLicense " . $trackLicense->track_license_id, 2 );
                    _report( "   license: " . Dumper($trackLicense),                                         3 );

                    my $share = $trackLicense->share;
                    _report( "    share % = $share", 2 );

                    # Calculate the effective units to pay on.
                    #
                    #                        my $grossUnits = $units;
                    #                        my $grossUnits = $numSales - $numReturns;
                    my $effectiveSales = $numSales;

                    # Calculate the effective rate
                    #
                    my ( $effectiveRate, $baseRate, $appliedStatRateID ) = _calculateEffectiveRate(
                        $issueStatRateID,         $saleStatRateID,           $trackLicense->rate_basis,
                        $trackLicense->rate_type, $trackLicense->penny_rate, $trackLicense->rate_percentage,
                        $duration,                $share,                    $trackLicense->lock_date
                    );
                    _report( "    effectiveRate = $effectiveRate", 2 );

                    # If there is a reserve specified, take it now (after other deductions)
                    #
                    # !!! However, if we're looking at _negative_ units, we can't take reserves from that!
                    #
                    my $amountReserved = 0;
                    my $unitsAfterReserve;
                    if ( $effectiveSales > 0 && !_productIsDigital($productID) ) {
                        $unitsAfterReserve = _applyDeduction( $effectiveSales, $trackLicense->reserve_percentage );
                        if ( $unitsAfterReserve < $effectiveSales ) {
                            $amountReserved = $effectiveSales - $unitsAfterReserve;
                            $effectiveSales = $unitsAfterReserve;
                        }
                        _report( "    effectiveSales after reserve % of " . $trackLicense->reserve_percentage . " : $effectiveSales", 2 );
                    } else {
                        _report( "    effective sales is $effectiveSales - no reserves taken", 2 );
                    }

                    # Now we can apply the liquidated units.
                    #
                    $effectiveSales += $liquidated;
                    _report( "   after applying liquidated units of $liquidated, effectiveSales = $effectiveSales", 2 );

                    if ( !$gClient->use_net_for_mechanical_deductions && $effectiveSales > 0 ) {
                        _report( "   NOT using net for mechanical deductions. effectiveSales before deductions: $effectiveSales", 2 );
                        $effectiveSales = _standardDeductions( $effectiveSales, $trackLicense );
                        _report( "     after deductions: $effectiveSales", 2 );
                    }

                    my $effectiveUnits = $effectiveSales - $numReturns;
                    _report( "   effectiveUnits = sale $effectiveSales - returns $numReturns = $effectiveUnits", 2 );

                    # See if there were any returned units held over from a previous run.
                    # (XXX) Going to just add these to the 'returns' column now.
                    #
                    # --- In the new cross-collateralization scheme, previousReturns
                    # --- will equate to the 'carryover' column.
                    #
                    if ($previousCarryover) {
                        $effectiveUnits += $previousCarryover;
                        _report( "   after applying previousCarryover of $previousCarryover, effectiveUnits = $effectiveUnits", 2 );
                    }

                    # !!! This seems totally wrong to me.
                    # !!! AND, nobody has that flag set anymore anyway...
                    #
                    if ( $gClient->use_net_for_mechanical_deductions && $effectiveUnits > 0 ) {
                        _report( "   using net for mechanical deductions. effectiveUnits before deductions: $effectiveUnits", 2 );
                        $effectiveUnits = _standardDeductions( $effectiveUnits, $trackLicense );
                        _report( "   after deductions: $effectiveUnits", 2 );
                    }

                    my $grossUnits = $effectiveUnits;

                    my $totalOwed;
                    my $amountPaid;
                    my $returnsReserved;

                    # Ok - back to where we were before.
                    # If the total owed is going to be less than 0, make it 0.
                    # We don't want to have negative dollar amounts.
                    # However, we are still going to to keep track of these negative
                    # units, which will be accounted for in later statements.
                    #
                    $totalOwed = Common::RSMath::round( $effectiveUnits * $effectiveRate, 2 );

                    $amountPaid = $totalOwed;

                    # (XXX) DISABLING CARRYOVER
                    #                        if ($amountPaid < 0)
                    #                        {
                    #                            $amountPaid = 0;
                    #                            $effectiveUnits = 0;
                    #                        }

                    _report( "    totalOwed: $totalOwed  amountPaid: $amountPaid", 2 );

                    # (XXX) This is going to be the new carryover balance.
                    #
                    my $newCarryover = 0;

                    # (XXX) DISABLING CARRYOVER
                    #                        if ($grossUnits < 0)
                    #                        {
                    #                            $newCarryover = $grossUnits;
                    #                        }

                    # Fetch the Statement.
                    # !!! _this_ needs to take payor_id
                    #
                    my $statement = _getStatement( $payorID, $publisherID, $startDate, $endDate );

                    # Create a statement item if one doesn't exist yet.
                    #
                    my $statementID   = $statement->mechanical_statement_id;
                    my $statementItem = _getStatementItem( $statementID, $productID, $issueStatRateID,
                        $saleStatRateID, $trackLicense->track_license_id, $saleIDs );

                    # Determine the HFA reporting period value.
                    #
                    my $hfaReportingPeriod =
                      _calculateHFAReportingPeriod( $startDate, $endDate, $issueStatRateID, $saleStatRateID, $trackLicense );

                    assert( $effectiveRate > 0 );

                    # Update the statement item
                    #
                    $statementItem->gross_units($grossUnits);
                    $statementItem->net_units($effectiveUnits);
                    $statementItem->net_rate($effectiveRate);
                    $statementItem->amount_paid($amountPaid);
                    $statementItem->total($totalOwed);
                    $statementItem->reserved($amountReserved);
                    $statementItem->hfa_reporting_period($hfaReportingPeriod);
                    $statementItem->applied_stat_rate_id($appliedStatRateID);
                    $statementItem->base_rate($baseRate);
                    $statementItem->sales($numSales);
                    $statementItem->returns($numReturns);
                    $statementItem->carryover($newCarryover);
                    $statementItem->liquidated($liquidated);
                    $statementItem->previous_carryover($previousCarryover);
                    $statementItem->save();

                    _report( "    created statement item\n", 2 );

                    # Save the reserved units someplace.
                    #
                    if ($amountReserved) {
                        my $statementItemID = $statementItem->mechanical_statement_item_id;

                        # Need to create all the reserve buckets at once.
                        #
                        my %scheduleTable;
                        my $schedule =
                          RPS::DB::Item::ReserveLiquidation->GetByIDType( $trackLicenseID,
                            RPS::DB::Item::ReserveLiquidation::kTypePublishingLicense );

                        while ( $schedule->hasNext() ) {
                            my $scheduleEntry = $schedule->next();
                            $scheduleTable{ $scheduleEntry->period } = $scheduleEntry->percent;
                        }

                        my $unitsReserved = $amountReserved;
                        my $initialUnits  = $unitsReserved;
                        for ( my $i = 1 ; $unitsReserved > 0 && $i <= 8 ; $i++ ) {
                            my $p = $scheduleTable{$i};
                            next unless $p;

                            my $periodUnits = ceil( $initialUnits * ( $p / 100 ) );
                            $periodUnits = $unitsReserved unless $periodUnits < $unitsReserved;

                            $unitsReserved -= $periodUnits;

                            my $newReserve = RPS::DB::Item::LicenseReserve->Create(
                                track_license_id           => $trackLicenseID,
                                original_statement_item_id => $statementItemID,
                                product_id                 => $productID,
                                sale_stat_rate_id          => $saleStatRateID,
                                issue_stat_rate_id         => $issueStatRateID,
                                units                      => $periodUnits,
                                effective_rate             => $effectiveRate,
                                periods_remaining          => $i + 1,
                            );
                            $newReserve->save();

                            # Associate this reserve with this run, so we can increment it's next period someday.
                            #
                            my $runMapItem = RPS::DB::Item::LicenseReserveRun->Create(
                                license_reserve_id => $newReserve->license_reserve_id,
                                mechanical_run_id  => $gRunID,
                            );
                            $runMapItem->save();
                        }
                    }

                    # JPK - I suppose this is the correct place to make note of the return balance?
                    # - No - Let's do that at another time, when we commit the statements.
                    #                        if ($newReturnBalance || $previousReturns)
                    #                        {
                    #                            _saveReturns($productID, $trackLicenseID, $newReturnBalance);
                    #                        }
                }
            }
        }
        $progressBar->increment();
        if ($gProgress) {
            $| = 1;
            $progressBar->display( \*STDOUT );
        }
    }

    # Grab all the distinct track licenses that have pending transactions.
    # We want to make sure we generate statements for these, even if there are no sales!
    #
    # We may end up with publishers that only have pending transactions, but no sales, reserves, adjustments, etc.
    # This means that they won't have any statement items by the time we get to the finalizeStatements function,
    # which means that we won't create a decent statement for them.
    # As a hack to get around that, I will keep track of the publisher/sub-publishers that have pending transactions
    # to make sure they get processed.
    #
    my $licensesWithPendingTransactions = RPS::DB::Item::TrackLicense->GetLicensesWithPendingTransactions();
    while ( my $tl = $licensesWithPendingTransactions->next() ) {

        # !!! We don't want to process transactions for inactive licenses...
        #
        next if ( $tl->inactive() );

        my $payorID = $tl->payor_id;
        next unless ( $payorIDMap->{$payorID} );

        # !!! This _might_ be enough - just force this publisher to have a statement.
        #
        my $publisherID = _getCorrectPublisherID($tl);
        _report( "Forcing a statement for publisher $publisherID because it has pending transactions", 2 );
        my $statement = _getStatement( $payorID, $publisherID, $startDate, $endDate );

        $gPendingTransPubMap{$payorID}{ $tl->publisher_id }{$publisherID} = 1;
    }

    # We need to do essentially the same thing for publishers that have publisher-level pending transactions, but
    # nothing else.
    #
    my $pubAccounts = RPS::DB::Item::PublisherAccount->GetAllWithPendingTransactions();
    while ( my $pa = $pubAccounts->next() ) {
        my $payorID = $pa->payor_id;
        next unless ( $payorIDMap->{$payorID} );
        my $paPublisher = $pa->publisher_id;
        _report( "fetching statement for publisher with pending transactions", 4 );
        my $statement = _getStatement( $payorID, $paPublisher, $startDate, $endDate );

        # !!! This is the part I don't understand.  Why do I do this?
        # !!! sub-publishers are not relevant.
        # !!! All I should _have_ to do is get the statement...
        #
        #        $gPendingTransPubMap{$payorID}{$paPublisher}{$paPublisher} = 1;
    }

    # Output the report of overages.
    #
    foreach my $overageProductID ( keys %overshareMap ) {
        my $trackHash = $overshareMap{$overageProductID};
        foreach my $overageTrackID ( keys %$trackHash ) {
            my $printableList = $trackHash->{$overageTrackID}{list};
            my $share         = $trackHash->{$overageTrackID}{share};
            _report(
                "SHARE OVERAGE: product_id:$overageProductID  track_id:$overageTrackID  shares: $share  license_id list:$printableList");
        }
    }

    #_report("SKIPPING THE LICENSE ACCRUAL REPORT");
    #return;

    # Create the missing license accrual report
    #
    _report( "DEBUG: MISSING SHARE MAP:" . Dumper( \%missingShareMap ) );

    #return;

    foreach my $trackID ( keys %missingShareMap ) {
        my $track = _getTrack($trackID);
        if ( defined($track) ) {
            my $album       = _getAlbum( $track->album_id );
            my $label       = _getLabel( $album->label_id );
            my $master      = _getMaster( $track->master_id );
            my $trackArtist = _getArtist( $track->artist_id );
            my $duration    = $master->duration;

            # !!! Can't assume that there _is_ a track artist, oddly.
            #
            my $trackArtistName = '';
            if ( defined($trackArtist) ) {
                $trackArtistName = $trackArtist->name;
            }

            my $totalOwed;
            my $totalShare;

            my $productIDHash = $missingShareMap{$trackID};
            foreach my $productID ( keys %$productIDHash ) {
                my ( $upc, $isrc ) = _getUPCAndISRCFromProductID($productID);

                my $product = _getProduct($productID);
                my $productTypeID;
                my $productType;
                if ($product) {
                    $productTypeID = $product->product_type_id;
                    $productType   = _getProductType($productTypeID);
                    $productType   = $productType->description;
                }
                my $saleStatRateIDHash = $missingShareMap{$trackID}{$productID};
                foreach my $saleStatRateID ( keys %$saleStatRateIDHash ) {
                    my $data = $saleStatRateIDHash->{$saleStatRateID};

                    my $units = $data->{units};
                    my $share = $data->{share};

                    my ($rate) = _calculateEffectiveRate(
                        16, $saleStatRateID,
                        RPS::DB::Item::TrackLicense::kRateBasisSale,
                        RPS::DB::Item::TrackLicense::kRateTypeFull,
                        0, 100, $duration, $share, 0
                    );

                    _report("DEBUG: track $trackID stat rate $saleStatRateID share $share UNITS: $units Rate returned: $rate");

                    $totalOwed += Common::RSMath::round( $units * $rate, 2 );

                    # Total share should not actually CHANGE
                    #
                    $totalShare = $share;
                }

                my $log = RPS::DB::Item::MechanicalRunUnlicensedTrackLog->Create(
                    mechanical_run_id => $gRunID,
                    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_order => $track->track_order,
                    track_artist_name => $trackArtistName,
                    track_title       => $track->title,

                    #track_length => $duration,
                    isrc             => $isrc,
                    upc_ean          => $upc,
                    product_type     => $productType,
                    unlicensed_share => $totalShare,
                    accrual          => $totalOwed
                );

                $log->save();

            }
        }
    }
}

sub _processControlledComp {
    my ( $controlledCompState, $startDate, $endDate ) = @_;

    my $progressBar = Common::TextProgressBar->new( scalar keys %$controlledCompState, kSaleProgressQuanta );
    if ($gProgress) {
        $| = 1;
        $progressBar->display( \*STDOUT );
    }

    foreach my $albumID ( keys %$controlledCompState ) {
        _report( "Processing controlled comps for album $albumID", 2 );

        my $ccIDHash = $controlledCompState->{$albumID};
        foreach my $ccID ( keys %$ccIDHash ) {
            _report( " Processing controlled comp $ccID", 2 );

            # (XXX) Where are these ids coming from?
            #
            my $controlledComp = RPS::DB::Item::ControlledComposition->Lookup( controlled_composition_id => $ccID );
            my $trackLicenseIDHash = $ccIDHash->{$ccID};
            foreach my $trackLicenseID ( keys %$trackLicenseIDHash ) {

                # Get the track license data, and then get the %share and publisher id from that.
                #
                my $trackLicense = RPS::DB::Item::TrackLicense->Lookup( track_license_id => $trackLicenseID );
                my $share        = $trackLicense->share;
                my $publisherID  = $trackLicense->publisher_id;
                my $payorID      = $trackLicense->payor_id;

                _report( "    publisher $publisherID", 2 );

                # If this track_license is not _direct_, we need to report this
                # through an agent or admin.
                #
                if ( !$trackLicense->publisher_direct ) {
                    my $publisherData = _getPublisher($publisherID);

                    # !!! This may be redundant, but better safe than sorry.
                    #
                    if ( $publisherData && RPS::DB::Item::Publisher::kStatusInactive == $publisherData->status() ) {
                        _report( "    this publisher is inactive, skipping", 3 );
                        next;
                    }

                    my $agentID = $publisherData->agent_id;
                    my $adminID = $publisherData->admin_id;
                    if ($agentID) {
                        $publisherID = $agentID;
                        _report( "    reporting controlled comp on agent $publisherID statement", 2 );
                    } elsif ($adminID) {
                        $publisherID = $adminID;
                        _report( "    reporting controlled comp on admin $publisherID statement", 2 );
                    }

                    # JPK - Note that if the agentID was not set, then we essentially
                    # ignore the publisherDirect flag (and treat it automatically as publisherDirect)
                    #
                }

                my $productIDHash = $trackLicenseIDHash->{$trackLicenseID};
                foreach my $productID ( keys %$productIDHash ) {
                    my $issueRateHash = $productIDHash->{$productID};
                    foreach my $issueStatRateID ( keys %$issueRateHash ) {
                        my $saleRateHash = $issueRateHash->{$issueStatRateID};
                        foreach my $saleStatRateID ( keys %$saleRateHash ) {
                            _report( "  productID: $productID issueStatRate: $issueStatRateID  saleStatRate: $saleStatRateID", 2 );

                            # calculate effective album rate.
                            #
                            my $baseRate = _calculateEffectivePerTrackRate( $albumID,
                                $issueStatRateID, $saleStatRateID, $controlledComp, $trackLicense );
                            _report( "  effective per track rate: $baseRate", 2 );

                            # now calculate actual amount, and save as a statement item
                            #
                            next unless $share > 0;

                            #                            my $units = $saleRateHash->{$saleStatRateID}{units};
                            my $numSales          = $saleRateHash->{$saleStatRateID}{numSales};
                            my $numReturns        = $saleRateHash->{$saleStatRateID}{numReturns};
                            my $units             = $numSales - $numReturns;
                            my $saleIDs           = $saleRateHash->{$saleStatRateID}{saleIDs};
                            my $liquidated        = $saleRateHash->{$saleStatRateID}{liquidated};
                            my $previousCarryover = $saleRateHash->{$saleStatRateID}{carryover};

                            my $netSales = $numSales;

                            # Take the reserve percentage into account.
                            # If a reserve percentage was specified, we'll have to 'set aside'
                            # units to be payed out later.
                            # !!! Don't take reserves on digital sales...
                            #
                            my $amountReserved = 0;
                            if ( !_productIsDigital($productID) && $netSales > 0 ) {
                                my $unitsAfterReserve = _applyDeduction( $netSales, $controlledComp->reserve_percentage );
                                if ( $unitsAfterReserve < $netSales ) {
                                    $amountReserved = $netSales - $unitsAfterReserve;
                                    $netSales       = $unitsAfterReserve;
                                }
                                _report(
                                    "    effective sales after ccomp reserve % of " . $controlledComp->reserve_percentage . " : $netSales",
                                    2
                                );
                            }

                            my $netUnits = $netSales - $numReturns;

                            # See if there were any units held over from a previous run.
                            #
                            if ($previousCarryover) {
                                $netUnits += $previousCarryover;
                            }

                            my $effectiveRate = _roundRate( $baseRate * ( $share / 100 ) );

                            # !!! If the rate is < 0, then we'll set the effective rate to 0
                            #
                            if ( $effectiveRate < 0 ) {
                                _report( " ======= rate is < 0: album_id $albumID  track_license_id $trackLicenseID", 2 );
                                $effectiveRate = 0;
                            }

                            # Any reserves to be liquidated?
                            #
                            $netUnits += $liquidated;

                            my $grossUnits = $netUnits;

                            my $total = Common::RSMath::round( $netUnits * $effectiveRate, 2 );
                            my $amountPaid = $total;

                            # (XXX) DISABLING CARRYOVER
                            #                            if ($total < 0)
                            #                            {
                            #                                $amountPaid = 0;
                            #                                $netUnits = 0;
                            #                            }

                          # !!! I don't see any accounting for the possibility of _negative_ rate, therefore carryover of positive units !!!

                            my $newCarryover = 0;

                            # (XXX) DISABLING CARRYOVER
                            #                            if ($grossUnits < 0)
                            #                            {
                            #                                $newCarryover = $grossUnits;
                            #                            }
                            _report(
"   gross units: $grossUnits  netUnits: $netUnits  share: $share  new carryover: $newCarryover total: $total paid: $amountPaid",
                                2
                            );

                            my $hfaReportingPeriod =
                              _calculateHFAReportingPeriod( $startDate, $endDate, $issueStatRateID, $saleStatRateID, $trackLicense );

                            # Create statement_item
                            #
                            my $statement = _getStatement( $payorID, $publisherID, $startDate, $endDate );
                            my $statementItem = _getStatementItem( $statement->mechanical_statement_id,
                                $productID, $issueStatRateID, $saleStatRateID, $trackLicenseID, $saleIDs );

                            _report( " +++++ creating statement item for CComp trackLicenseID = $trackLicenseID", 3 );

                            #                            assert($effectiveRate > 0);

                            # Update the statement item
                            #
                            $statementItem->net_units($netUnits);
                            $statementItem->gross_units($grossUnits);
                            $statementItem->net_rate($effectiveRate);    # !!! Might be good to sanity-check this.
                            $statementItem->total($total);
                            $statementItem->amount_paid($amountPaid);
                            $statementItem->base_rate($baseRate);        # jpk - not sure this really means anything
                            $statementItem->reserved($amountReserved);
                            $statementItem->sales($numSales);
                            $statementItem->returns($numReturns);
                            $statementItem->carryover($newCarryover);
                            $statementItem->liquidated($liquidated);
                            $statementItem->previous_carryover($previousCarryover);
                            $statementItem->hfa_reporting_period($hfaReportingPeriod);

                            $statementItem->save();

                            # Keep track of any reserved units.
                            #
                            if ($amountReserved) {
                                my $statementItemID = $statementItem->mechanical_statement_item_id;

                                # Need to create all the reserve buckets at once.
                                #
                                my %scheduleTable;
                                my $schedule =
                                  RPS::DB::Item::ReserveLiquidation->GetByIDType( $ccID,
                                    RPS::DB::Item::ReserveLiquidation::kTypeControlledComposition );

                                while ( $schedule->hasNext() ) {
                                    my $scheduleEntry = $schedule->next();
                                    $scheduleTable{ $scheduleEntry->period } = $scheduleEntry->percent;
                                }

                                my $unitsReserved = $amountReserved;
                                my $initialUnits  = $unitsReserved;
                                for ( my $i = 1 ; $unitsReserved > 0 && $i <= 8 ; $i++ ) {
                                    my $p = $scheduleTable{$i};
                                    next unless $p;

                                    my $periodUnits = ceil( $initialUnits * ( $p / 100 ) );
                                    $periodUnits = $unitsReserved unless $periodUnits < $unitsReserved;

                                    $unitsReserved -= $periodUnits;

                                    my $newReserve = RPS::DB::Item::LicenseReserve->Create(
                                        track_license_id           => $trackLicenseID,
                                        original_statement_item_id => $statementItemID,
                                        product_id                 => $productID,
                                        sale_stat_rate_id          => $saleStatRateID,
                                        issue_stat_rate_id         => $issueStatRateID,
                                        units                      => $periodUnits,
                                        effective_rate             => $effectiveRate,
                                        periods_remaining          => $i + 1,
                                    );
                                    $newReserve->save();

                                    # Associate this reserve with this run, so we can increment it's next period someday.
                                    #
                                    my $runMapItem = RPS::DB::Item::LicenseReserveRun->Create(
                                        license_reserve_id => $newReserve->license_reserve_id,
                                        mechanical_run_id  => $gRunID,
                                    );
                                    $runMapItem->save();
                                }
                            }

                            # JPK - I suppose this is the correct place to make note of the return balance?
                            #
                            #                            if ($newReturnBalance || $previousReturns)
                            #                            {
                            #                                _saveReturns($productID, $trackLicenseID, $newReturnBalance);
                            #                            }
                        }
                    }
                }
            }
        }
        $progressBar->increment();
        if ($gProgress) {
            $| = 1;
            $progressBar->display( \*STDOUT );
        }
    }
}

sub _finalizeStatements {
    my ( $startDate, $endDate ) = @_;
    _report( "Processing Track Subtotals", 2 );

    # Gather up the Statements that were just generated.
    # (This is kind of a hack, using a global like this...)
    #
    foreach my $payorID ( keys %$gPublisherStatementMap ) {
        my $publisherIDStatementHash = $gPublisherStatementMap->{$payorID};
        foreach my $publisherID ( keys %$publisherIDStatementHash ) {
            _report( "  Publisher id $publisherID", 2 );
            my $publisherData = _getPublisher($publisherID);
            if ( $publisherData && RPS::DB::Item::Publisher::kStatusInactive == $publisherData->status() ) {
                _report( "   This publisher is inactive, skipping", 3 );
                next;
            }

            # !!! This is probably redundant, but skip inactive publishers
            #

            my $publisherSubtotal = 0;
            my $digitalSubtotal   = 0;
            my $physicalSubtotal  = 0;
            my $statement         = $publisherIDStatementHash->{$publisherID};
            my $statementID       = $statement->mechanical_statement_id;

            # So... Need to make some more changes to account for the multi-publisher
            # statements.
            # Going to refer to 'subPublishers' to handle that.
            # Need to determine all the publishers who are reporting through a statement.
            #
            my @subPublishers = RPS::DB::Item::MechanicalStatement->GetPublishers($statementID);

            # !!! If we are here because of pending transactions, but no sales, then there won't be
            # !!! any publishers in the 'subPublishers' array.  Which is annoying.
            #
            # So, we'll use this hash table hack to make sure they get looked at.
            #
            #        $gPendingTransPubMap{$payorID}{$tl->publisher_id}{$publisherID} = 1;
            if ( $gPendingTransPubMap{$payorID}{$publisherID} ) {
                foreach my $pendingSubPubID ( keys %{ $gPendingTransPubMap{$payorID}{$publisherID} } ) {
                    my $addFlag = 1;
                    foreach my $testSub (@subPublishers) {
                        if ( $testSub == $pendingSubPubID ) {
                            $addFlag = 0;
                            last;
                        }
                    }
                    if ($addFlag) {
                        _report( "*****   adding publisher $pendingSubPubID to the subPublishers array", 2 );
                        push @subPublishers, $pendingSubPubID;
                    }
                }
            }

            foreach my $subPublisherID (@subPublishers) {
                my %trackLicenseHash;
                my $subPublisherSubtotal;

                # See if there is a crossed-license account balance.
                # If there is, we want to make sure that gets processed even if there
                # are no active licenses on this track...
                #
                my $allCrossedAccountMaps =
                  RPS::DB::Item::PublisherTrackCrossedLicenseAccount->GetForPublisherPayor( $subPublisherID, $payorID );
                while ( my $map = $allCrossedAccountMaps->next() ) {
                    $trackLicenseHash{ $map->track_id } = {};
                }

                # Gather up the line items on this statement, and accumulate the totals by license.
                # (XXX) I am going to have to do this _twice_ for the cross-collateralization.
                # (XXX) Because, I need to make adjustments to all the statement items associated
                # (XXX) with a track _before_ I sum everything up...
                #
                _report( "    Accumulating totals", 2 );
                my $statementItems =
                  RPS::DB::Item::MechanicalStatementItem->GetByMechanicalStatementIDPublisherID( $statementID, $subPublisherID );
                while ( $statementItems->hasNext() ) {
                    my $statementItem = $statementItems->next();

                    # Fetch the license if we haven't already.
                    #
                    my $trackLicenseID = $statementItem->track_license_id;
                    my $trackLicense   = RPS::DB::Item::TrackLicense->Lookup( track_license_id => $trackLicenseID );
                    my $trackID        = $trackLicense->track_id;
                    if ( !defined $trackLicenseHash{$trackID}{$trackLicenseID} ) {
                        $trackLicenseHash{$trackID}{$trackLicenseID}{license} = $trackLicense;
                    }

                    # We need to report the subtotals of physical and digital revenue seperately.
                    # (XXX) amount_paid, or total?  I believe it's amount_paid, but I could be wrong...
                    #
                    if ( _productIsDigital( $statementItem->product_id ) ) {
                        $digitalSubtotal += Common::RSMath::round( $statementItem->amount_paid, 2 );
                    } else {
                        $physicalSubtotal += Common::RSMath::round( $statementItem->amount_paid, 2 );
                    }

                    # Fetch the total that appears on this line item.
                    #
                    $trackLicenseHash{$trackID}{$trackLicenseID}{amount} += Common::RSMath::round( $statementItem->amount_paid, 2 );
                    _report(
                        " for trackLicenseHash $trackID $trackLicenseID : amount = "
                          . $statementItem->amount_paid
                          . " , current total = "
                          . $trackLicenseHash{$trackID}{$trackLicenseID}{amount},
                        2
                    );
                }

                # Now grab all the mechanical statement adjustments, so we can include their amounts in our subtotals.
                #
                _report( "    Applying adjustments", 2 );

                my $adjustmentItems =
                  RPS::DB::Item::MechanicalStatementAdjustmentItem->GetByMechanicalStatementIDPublisherID( $statementID, $subPublisherID );
                _report( " number of adjustment items: " . $adjustmentItems->size(), 2 );
                while ( $adjustmentItems->hasNext() ) {
                    my $adjustmentItem = $adjustmentItems->next();

                    my $trackLicenseID = $adjustmentItem->track_license_id;
                    my $trackLicense   = RPS::DB::Item::TrackLicense->Lookup( track_license_id => $trackLicenseID );
                    my $trackID        = $trackLicense->track_id;
                    _report( " trackLicenseID : $trackLicenseID  trackID : $trackID", 3 );
                    if ( !defined $trackLicenseHash{$trackID}{$trackLicenseID} ) {
                        $trackLicenseHash{$trackID}{$trackLicenseID}{license} = $trackLicense;
                    }

                    # Increment the total owed on this license.
                    #
                    _report( " adding " . $adjustmentItem->amount . " to licenseHash amount", 2 );
                    $trackLicenseHash{$trackID}{$trackLicenseID}{amount} += Common::RSMath::round( $adjustmentItem->amount, 2 );

                }

                # We might be here because this publisher had a track license with a pending transaction,
                # but no sales.
                # So, if that's the case, we'll need to 'seed' the trackLicenseHash
                #
                # $payorID, $subPublisherID
                my $tlsWithPending = RPS::DB::Item::TrackLicense->GetLicensesWithPendingTransactions();
                while ( my $tl = $tlsWithPending->next() ) {
                    next if ( $tl->inactive() );

                    # !!! Yes, this is gross and inefficient...
                    #
                    if (   $tl->payor_id == $payorID
                        && $tl->publisher_id == $subPublisherID )

                      #                 && $tl->publisher_id == $publisherID)
                    {
                        if ( !defined $trackLicenseHash{ $tl->track_id }{ $tl->track_license_id } ) {
                            _report(
                                "++++++ adding track "
                                  . $tl->track_id
                                  . " : license "
                                  . $tl->track_license_id
                                  . " to hash to catch pending transactions",
                                2
                            );
                            $trackLicenseHash{ $tl->track_id }{ $tl->track_license_id }{license} = $tl;
                        }
                    }
                }

                # Now we can iterate over all the affected licenses, and process totals and advances for each.
                #
                _report( "    Processing totals", 2 );
                foreach my $trackID ( keys %trackLicenseHash ) {
                    my $trackTotal  = 0;
                    my $licenseHash = $trackLicenseHash{$trackID};

                    my $crossedTransactionSubtotal;
                    my $crossedSubtotal;
                    my $crossedPreviousAdvanceBalance;
                    my $crossedAppliedToAdvance;
                    my $crossedAdvanceBalance;
                    my $crossedAdjustedSubtotal;
                    my $crossedTotal;

                    _report( "    track $trackID", 2 );
                    foreach my $trackLicenseID ( keys %$licenseHash ) {
                        my $license = $licenseHash->{$trackLicenseID}{license};
                        next if ( $license->inactive() );

                        my $subtotal                   = Common::RSMath::round( $licenseHash->{$trackLicenseID}{amount}, 2 );
                        my $total                      = 0;
                        my $previousAdvanceBalance     = 0;
                        my $pendingTransactionSubtotal = 0;
                        my $advanceBalance             = 0;
                        my $appliedToAdvance           = 0;

                        my $isCrossed = ( $license ? $license->cross_collateralized : 0 );

                        _report( "      subtotal for trackLicenseID $trackLicenseID = $subtotal", 2 );

                        # Create and save the license item.  We'll be filling it in with more
                        # data later, but I (might) need the new ID in the meanwhile...
                        #
                        my $newLine = RPS::DB::Item::MechanicalStatementLicense->Create(
                            mechanical_statement_id => $statementID,
                            track_license_id        => $trackLicenseID,
                        );
                        $newLine->save();
                        my $newLicenseItemID = $newLine->mechanical_statement_license_id;

                        if ( !$license ) {

                            # Nothing fancy with controlled comp (or unknown publisher) - The total is the subtotal.
                            #
                            $total = $subtotal;
                            _report( "      controlled comp or unknown publisher : total = $total", 2 );
                        } else {

                            # Fetch the initial advance amount from the license, then try and fetch
                            # the license's advance account.   If we haven't set up that account yet,
                            # then the advance amount will just be the initial advance.
                            # (We won't actually create the account or post any transactions yet - That
                            # will happen at some future date when the statement has been accepted.)
                            #
                            my $accountID = $license->finance_account_id();
                            my $pendingTransactions;
                            $previousAdvanceBalance = 0;
                            if ($accountID) {

                                # Get the current advance account balance.
                                #
                                $previousAdvanceBalance = RPS::DB::Item::FinanceAccount->CurrentBalance($accountID);
                                $pendingTransactions =
                                  RPS::DB::Item::PendingTransaction->GetAccountTransactions( finance_account_id => $accountID );
                                _report( "      advance account current balance = $previousAdvanceBalance", 2 );
                            } else {
                                _report( "      no advance account established . advance amount = $previousAdvanceBalance", 2 );
                            }

                            # Does this account have pending transactions?  If so, slurp them up.
                            #
                            if ( $pendingTransactions && $pendingTransactions->size() > 0 ) {
                                _report( "++++++   we have pending transactions", 2 );
                                while ( my $pendingTrans = $pendingTransactions->next() ) {
                                    $pendingTransactionSubtotal += $pendingTrans->amount;

                                    my $licenseTransItem = RPS::DB::Item::MechanicalStatementLicenseTransaction->Create(
                                        'mechanical_statement_license_id' => $newLicenseItemID,
                                        'amount'                          => $pendingTrans->amount,
                                        'memo'                            => $pendingTrans->memo,
                                        'check_number'                    => $pendingTrans->check_number,
                                        'pending_transaction_id'          => $pendingTrans->pending_transaction_id,
                                        'transaction_date'                => $pendingTrans->transaction_date,
                                        'type_code'                       => $pendingTrans->type_code,
                                    );

                                    $licenseTransItem->save();
                                }
                            }

                            # !!! Need to modify this logic.  This account is no longer just the 'advance'.
                            # !!! It's whatever the running total might happen to be.
                            # !!! So... we're going to have to touch this again when we re-do the
                            # !!! carryover code.
                            #
                            # subtotal - amount due before account factored in.
                            #
                            my $adjustedSubtotal = $subtotal + $previousAdvanceBalance + $pendingTransactionSubtotal;

                            # Here we diverge for crossed licenses.
                            #
                            if ($isCrossed) {

                                # For crossed licenses, we don't carry the balance forward at the license level.
                                # Instead that will pour into the crossed bucket.
                                # But we do need to 'zero out' any pre-existing balance.
                                #
                                $appliedToAdvance = ( -1 * $previousAdvanceBalance );
                                $advanceBalance   = 0;
                                $total            = $adjustedSubtotal;

                                $crossedSubtotal += $total;
                            } else {

                                # If the adjusted subtotal <=0, then we are not going to be paying anything
                                # this time around. We'll instead be carrying forward that amount as a balance.
                                #
                                if ( $adjustedSubtotal > 0 ) {
                                    $advanceBalance = 0;
                                    $total          = $adjustedSubtotal;
                                } else {
                                    $advanceBalance += $adjustedSubtotal;
                                    $total = 0;
                                }

                                $appliedToAdvance = $advanceBalance - $previousAdvanceBalance;

                                _report(
"  prev advance: $previousAdvanceBalance - new advance balance: $advanceBalance subtotal: $subtotal : adjusted subtotal $adjustedSubtotal total $total",
                                    2
                                );
                            }
                        }

                        # Save the results of all this calculating...
                        #
                        $newLine->previous_advance_balance($previousAdvanceBalance);
                        $newLine->advance_balance($advanceBalance);
                        $newLine->applied_to_advance($appliedToAdvance);
                        $newLine->transaction_subtotal($pendingTransactionSubtotal);
                        $newLine->adjusted_subtotal($total);
                        $newLine->subtotal($subtotal);
                        $newLine->crossed($isCrossed);

                        # Get the region id from track license
                        # !!! We may want to expand the stuff that we store in the mechanical_statement_license,
                        # !!! to avoid backreferencing the track license when we render the statements.
                        #
                        _report("!!!!!!!!! about to fetch trackLicenseID $trackLicenseID");
                        my $trackLicense = RPS::DB::Item::TrackLicense->Lookup( track_license_id => $trackLicenseID );
                        if ( $trackLicense->region_id ) {
                            _report( "!!!!!!!!!  setting region_id to " . $trackLicense->region_id );
                            $newLine->region_id( $trackLicense->region_id );
                        }

                        $newLine->save();

                        # We'll account for the crossed money later.
                        #
                        if ( !$isCrossed ) {
                            $trackTotal += $total;
                        }
                        _report( "   total $total  trackTotal $trackTotal", 2 );
                    }

                    # Now see if there is a crossed balance account.
                    #
                    my $crossedAccountMap = RPS::DB::Item::PublisherTrackCrossedLicenseAccount->Lookup(
                        publisher_id => $subPublisherID,
                        track_id     => $trackID,
                        payor_id     => $payorID,
                    );
                    if ($crossedAccountMap) {
                        my $accountID = $crossedAccountMap->finance_account_id;

                        my $pendingTransactions;
                        $crossedPreviousAdvanceBalance = 0;
                        if ($accountID) {

                            # Get the current advance account balance.
                            #
                            $crossedPreviousAdvanceBalance = RPS::DB::Item::FinanceAccount->CurrentBalance($accountID);
                            $pendingTransactions =
                              RPS::DB::Item::PendingTransaction->GetAccountTransactions( finance_account_id => $accountID );
                        }

                        # Does this account have pending transactions?  If so, slurp them up.
                        #
                        if ( $pendingTransactions && $pendingTransactions->size() > 0 ) {
                            while ( my $pendingTrans = $pendingTransactions->next() ) {
                                $crossedTransactionSubtotal += $pendingTrans->amount;

                                my $licenseTransItem = RPS::DB::Item::MechanicalStatementCrossedLicenseTransaction->Create(

                                    #	                                'mechanical_statement_license_id'   => $newLicenseItemID,
                                    'mechanical_statement_id' => $statementID,
                                    'publisher_id'            => $subPublisherID,
                                    'track_id'                => $trackID,
                                    'amount'                  => $pendingTrans->amount,
                                    'memo'                    => $pendingTrans->memo,
                                    'check_number'            => $pendingTrans->check_number,
                                    'pending_transaction_id'  => $pendingTrans->pending_transaction_id,
                                    'transaction_date'        => $pendingTrans->transaction_date,
                                    'type_code'               => $pendingTrans->type_code,
                                );

                                $licenseTransItem->save();
                            }
                        }

                    }
                    $crossedAdjustedSubtotal = $crossedSubtotal + $crossedPreviousAdvanceBalance + $crossedTransactionSubtotal;
                    if ( $crossedAdjustedSubtotal > 0 ) {
                        $crossedAdvanceBalance = 0;
                        $crossedTotal          = $crossedAdjustedSubtotal;
                    } else {
                        $crossedAdvanceBalance += $crossedAdjustedSubtotal;
                        $crossedTotal = 0;
                    }

                    $crossedAppliedToAdvance = $crossedAdvanceBalance - $crossedPreviousAdvanceBalance;

                    $trackTotal += $crossedTotal;

                    # Now save the totals for this track in yet another db table.
                    #

                    # In order to save time during rendering, we'll fetch the song title
                    # here and save it in the mechanical_statement_track table.
                    #
                    # Going to also fetch the album_id. (Maybe the album name would be more useful?)
                    #
                    my $albumID   = 0;
                    my $albumData = RPS::DB::Item::Album->GetByTrackID($trackID);
                    if ($albumData) {
                        $albumID = $albumData->album_id;
                    }
                    my $songData = _getSongByTrackID($trackID);
                    if ( !$songData ) { die "ERROR!!!! could not get any song data for track $trackID"; }

                    _report( "   creating track entry : track id = $trackID album id = $albumID track total = $trackTotal", 2 );
                    my $trackData = RPS::DB::Item::MechanicalStatementTrack->Create(
                        mechanical_statement_id          => $statementID,
                        publisher_id                     => $subPublisherID,
                        track_id                         => $trackID,
                        album_id                         => $albumID,
                        subtotal                         => $trackTotal,
                        song_title                       => $songData->title,
                        crossed_transaction_subtotal     => $crossedTransactionSubtotal,
                        crossed_subtotal                 => $crossedSubtotal,
                        crossed_previous_advance_balance => $crossedPreviousAdvanceBalance,
                        crossed_applied_to_advance       => $crossedAppliedToAdvance,
                        crossed_advance_balance          => $crossedAdvanceBalance,
                        crossed_adjusted_subtotal        => $crossedAdjustedSubtotal,
                    );

                    $trackData->save();
                    $publisherSubtotal    += $trackTotal;
                    $subPublisherSubtotal += $trackTotal;
                }

                # Create the publisher entry here.
                #
                my $subPublisherItem = RPS::DB::Item::MechanicalStatementPublisher->Create(
                    mechanical_statement_id => $statementID,
                    publisher_id            => $subPublisherID,
                    subtotal                => $subPublisherSubtotal,
                );

                $subPublisherItem->save();
            }

            # Now we _almost_ have the grand total.
            # We need to now apply the publisher advance, and take the
            # running balance into account.
            #
            my $publisherPreviousBalance  = 0;
            my $publisherBalance          = 0;
            my $publisherAppliedToBalance = 0;
            my $minPayment                = 0;
            my $publisherTotal            = 0;
            my $transactionSubtotal       = 0;

            # JPK - I'm combining the advance account and the running balance account, so
            #       some of this state is going to go away.
            #
            my $publisherAccountMap = RPS::DB::Item::PublisherAccount->Lookup(
                publisher_id => $publisherID,
                payor_id     => $payorID,
            );

            if ($publisherAccountMap) {
                $minPayment = $publisherAccountMap->min_payment;

                my $publisherAccountID = $publisherAccountMap->finance_account_id;
                my $pendingTransactions;
                if ($publisherAccountID) {
                    $publisherPreviousBalance = RPS::DB::Item::FinanceAccount->CurrentBalance($publisherAccountID);
                    $pendingTransactions =
                      RPS::DB::Item::PendingTransaction->GetAccountTransactions( finance_account_id => $publisherAccountID );
                }

                # Does this account have pending transactions?  If so, slurp them up.
                #
                if ( $pendingTransactions && $pendingTransactions->size() > 0 ) {
                    while ( my $pendingTrans = $pendingTransactions->next() ) {

                        # !!! Are these positive or negative?
                        # !!! They are what they are - any sign flipping is handled when the records are created.
                        #
                        $transactionSubtotal += $pendingTrans->amount;

                        my $statementTransItem = RPS::DB::Item::MechanicalStatementTransaction->Create(
                            'mechanical_statement_id' => $statementID,
                            'amount'                  => $pendingTrans->amount,
                            'memo'                    => $pendingTrans->memo,
                            'check_number'            => $pendingTrans->check_number,
                            'pending_transaction_id'  => $pendingTrans->pending_transaction_id,
                            'transaction_date'        => $pendingTrans->transaction_date,
                            'type_code'               => $pendingTrans->type_code,
                        );
                        $statementTransItem->save();
                    }
                }
            }

            _report( "\n****** publisher_id $publisherID", 3 );

            $publisherTotal = $publisherSubtotal + $publisherPreviousBalance + $transactionSubtotal;
            _report(
"      pubTotal $publisherTotal =  pubSubtotal $publisherSubtotal + prev $publisherPreviousBalance + pending $transactionSubtotal",
                3
            );

            my $statementTotal = $publisherTotal;

            _report( "      statementTotal: $statementTotal", 3 );

            my $amountDue = $statementTotal;
            if ( $amountDue < $minPayment ) {
                $amountDue = 0;
            }

            my $onHold = 0;
            if ( RPS::DB::Item::Publisher::kStatusOnHold == $publisherData->status() ) {
                $onHold    = 1;
                $amountDue = 0;
            }
            _report(
"   SUMMARY: subtotal $publisherSubtotal prev bal: $publisherPreviousBalance  adv bal: $publisherBalance  applied: $publisherAppliedToBalance statement total $statementTotal  minPayment $minPayment  due $amountDue",
                3
            );

            $statement->subtotal($publisherSubtotal);
            $statement->physical_subtotal($physicalSubtotal);
            $statement->digital_subtotal($digitalSubtotal);
            $statement->transaction_subtotal($transactionSubtotal);

            #        $statement->previous_advance_balance($publisherPreviousAdvanceBalance);
            #        $statement->advance_balance($publisherAdvanceBalance);
            #        $statement->applied_to_advance($publisherAppliedToAdvance);

            $statement->min_payment($minPayment);

            $statement->previous_balance($publisherPreviousBalance);
            $statement->balance($publisherBalance);
            $statement->applied_to_balance($publisherAppliedToBalance);

            $statement->statement_total($statementTotal);
            $statement->amount_due($amountDue);
            $statement->on_hold($onHold);

            $statement->save();
        }
    }
}

sub _markStatementsAsPending() {

    foreach my $payorID ( keys %$gPublisherStatementMap ) {
        my $pubIDMap = $gPublisherStatementMap->{$payorID};
        foreach my $publisherID ( keys %$pubIDMap ) {
            my $statement = $gPublisherStatementMap->{$payorID}{$publisherID};
            $statement->status(RPS::DB::Item::MechanicalStatement::kStatusPending);

            _report( "  statement id " . $statement->mechanical_statement_id . " is pending" );
            $statement->save();
        }
    }
}

# Assume deduction is a percentage: i.e. 10.0 == 10%
#
sub _applyDeduction {
    my ( $units, $deduction ) = @_;

    return $units if !$deduction;

    # calculate the deduction amount.
    #
    my $amountToDeduct = Common::RSMath::round( $units * ( $deduction / 100 ), 0 );
    $units -= $amountToDeduct;

    return $units;
}

# It's dumb to hit the database for these stat rate tables.
# Better to read the whole table into memory first, then
# read from ram.

sub _getStatRateTable {
    if ( !$gStatRateTable ) {
        $gStatRateTable = {};

        my $statRates = RPS::DB::Item::StatRate->GetAll();
        while ( my $statRate = $statRates->next() ) {
            $gStatRateTable->{ $statRate->stat_rate_id } = $statRate;
        }
    }

    return $gStatRateTable;
}

# Generally we are going to be asking for the same stat rate for the same
# date over and over and over and over... so it makes sense to cache it.
#
sub _getStatRateID {
    my $endDate = shift;

    if ( !$gStatRateByDate->{$endDate} ) {
        my $table = _getStatRateTable();

        # get the stat rates, sorted by date
        #
        my @sortedRateIDs = sort { $table->{$b}->date_effective <=> $table->{$a}->date_effective } keys %$table;
        foreach my $testRateID (@sortedRateIDs) {
            if ( $table->{$testRateID}->date_effective <= $endDate ) {
                $gStatRateByDate->{$endDate} = $table->{$testRateID}->stat_rate_id;
                last;
            }
        }
    }
    return $gStatRateByDate->{$endDate};
}

sub _getStatRate {
    my $statRateID = shift;

    my $table    = _getStatRateTable();
    my $statRate = $table->{$statRateID};
    assert($statRate);

    return ( $statRate->rate, $statRate->minute_rate );
}

sub _calculateEffectiveRate {
    my ( $issueStatRateID, $saleStatRateID, $rateBasis, $rateType, $pennyRate, $percentOfRate, $duration, $share, $lockDate ) = @_;

    _report(
"_calculateEffectiveRate : issueStatRateID = $issueStatRateID, saleStatRateID = $saleStatRateID, rateBasis = $rateBasis, pennyRate = $pennyRate, percentOfRate = $percentOfRate, duration = $duration, share = $share",
        3
    );

    # Calculate the effective rate per unit to pay.
    # rate basis - either sale or release
    # rate term - full, partial, or penny
    #
    my $appliedStatRateID;
    my ( $baseStatRate, $minuteRate );
    my $effectiveRate;

    if ( RPS::DB::Item::TrackLicense::kRateTypePenny == $rateType ) {
        $effectiveRate     = $pennyRate;
        $appliedStatRateID = $saleStatRateID;
        _report( "penny rate : minimum, pennyRate = $pennyRate", 3 );
    } else {
        if ( RPS::DB::Item::TrackLicense::kRateBasisSale == $rateBasis ) {
            ( $baseStatRate, $minuteRate ) = _getStatRate($saleStatRateID);
            _report( "rate basis = sale, baseRate = $baseStatRate, minute rate = $minuteRate", 3 );
            $appliedStatRateID = $saleStatRateID;
        }

        # !!! For this to convert to 'lock date', I'm going to _NEED_ to know the lock date. duh!
        elsif ( RPS::DB::Item::TrackLicense::kRateBasisLock == $rateBasis ) {
            my $lockStatRateID = _getStatRateID($lockDate);
            ( $baseStatRate, $minuteRate ) = _getStatRate($lockStatRateID);
            _report( "rate basis = lock, baseRate = $baseStatRate, minute rate = $minuteRate", 3 );
            $appliedStatRateID = $lockStatRateID;
        } else {
            croak "!!! Unknown rate basis: " . $rateBasis;
        }

        if ( RPS::DB::Item::TrackLicense::kRateTypeFull == $rateType ) {

            # Use the base stat rate for songs of 5 minutes or less.
            # Use the minute rate for longer songs.
            #
            $effectiveRate = $baseStatRate;
            if ( $duration > 300 ) {
                my $minutes      = ceil( $duration / 60 );
                my $minBasedRate = ( $minutes * $minuteRate );

                _report( "full rate. minutes = $minutes, duration = $duration, minute based rate = $minBasedRate", 3 );
                $effectiveRate = ( $minBasedRate > $effectiveRate ? $minBasedRate : $effectiveRate );
            }
        } elsif ( RPS::DB::Item::TrackLicense::kRateTypeMinimum == $rateType ) {
            $effectiveRate = $baseStatRate;
            _report( "min rate : minimum, baseStatRate = $baseStatRate", 3 );
        } else {
            croak "!!! Unknown rate type: " . $rateType;
        }
    }

    my $baseRate = $effectiveRate;

    # 0 %  is not an option...
    #
    if ( $percentOfRate > 0 && RPS::DB::Item::TrackLicense::kRateTypePenny != $rateType ) {
        $effectiveRate = $effectiveRate * ( $percentOfRate / 100 );
    }
    assert( $effectiveRate > 0 );

    # Now that we have the effective rate, cut it according to the share percentage.
    #
    if ( RPS::DB::Item::TrackLicense::kRateTypePenny != $rateType ) {
        $effectiveRate = $effectiveRate * ( $share / 100 );
    }

    # For my final trick - round these down to 4 places right of the decimal point.
    #
    $effectiveRate = _roundRate($effectiveRate);
    $baseRate      = _roundRate($baseRate);

    _report( "returning effective rate = $effectiveRate", 3 );
    return ( $effectiveRate, $baseRate, $appliedStatRateID );
}

sub _getStatement {
    my ( $payorID, $publisherID, $startDate, $endDate ) = @_;

    _report( "retrieving statement for publisher $publisherID  payor $payorID", 4 );
    my $statement = $gPublisherStatementMap->{$payorID}{$publisherID};
    if ( !$statement ) {
        $statement = RPS::DB::Item::MechanicalStatement->Lookup(
            payor_id          => $payorID,
            publisher_id      => $publisherID,
            start_date        => $startDate,
            end_date          => $endDate,
            mechanical_run_id => $gRunID,
        );

        if ( !$statement ) {
            $statement = RPS::DB::Item::MechanicalStatement->Create(
                payor_id          => $payorID,
                publisher_id      => $publisherID,
                mechanical_run_id => $gRunID,
                status            => RPS::DB::Item::MechanicalStatement::kStatusCreating,
                start_date        => $startDate,
                version           => 2,
                end_date          => $endDate
            );
            $statement->save();
        } else {

            # !!! Not sure what status are actually acceptable.
            # It occurs to me that even if we set the state to kStatusCreating,
            # we might still end up duplicating line items.
            # However, let's go ahead and allow kStatusCreating to be re-run, and assume
            # that we will have some way of catching dups at the Sale level.
            #
            if ( RPS::DB::Item::MechanicalStatement::kStatusCreating != $statement->status ) {

                # Maybe _dying_ is a bit drastic...
                #
                die "Error - You have already created a statement for payor $payorID publisher $publisherID for this period";
            }
        }

        $gPublisherStatementMap->{$payorID}{$publisherID} = $statement;
    }
    return $statement;
}

sub _getStatementItem {
    my ( $mechanicalStatementID, $productID, $issueStatRateID, $saleStatRateID, $trackLicenseID, $saleIDs ) = @_;

    my $statementItem = RPS::DB::Item::MechanicalStatementItem->Lookup(
        mechanical_statement_id => $mechanicalStatementID,
        track_license_id        => $trackLicenseID,
        product_id              => $productID,
        issue_stat_rate_id      => $issueStatRateID,
        sale_stat_rate_id       => $saleStatRateID
    );

    if ( !$statementItem ) {

        # jpk - we need to report upc.
        # This is stored in the product table.
        # I really don't want to have to query the product table when
        # we 'render' this statement item.
        #
        # So, I'm going to make upc a field in MechanicalStatementItem, and look up
        # the value here.
        #
        # Also need the ISRC value.
        # This is stored in the Master table...
        #
        my ( $upc, $isrc ) = _getUPCAndISRCFromProductID($productID);

        my $service;

        # (XXX)
        # This is poorly thought out.  And, it looks like service_id is _always_ NULL.  So punt.
        # In the future, we should probably have service_id be a unique key into MechanicalStatementItem.
        # It seems that we'd want a seperate line item for each service (to satisy HFA, mostly).
        #
        #        if ($saleID)
        #        {
        #            my $sale = Raptor::DB::Item::Sale->Lookup(sale_id => $saleID);
        #            my $serviceID = $sale->service_id;
        #            if (! $serviceID)
        #            {
        #                $serviceID = _fileIDToService($sale->file_id);
        #            }
        #            $service = _serviceIDToName($serviceID);
        #        }

        $statementItem = RPS::DB::Item::MechanicalStatementItem->Create(
            mechanical_statement_id => $mechanicalStatementID,
            track_license_id        => $trackLicenseID,
            product_id              => $productID,
            issue_stat_rate_id      => $issueStatRateID,
            upc                     => $upc,

            #            service => $service,
            isrc              => $isrc,
            sale_stat_rate_id => $saleStatRateID
        );

        # Have to save it now, so we can get the id
        #
        $statementItem->save();
    }

    # create entry in stmt map table
    # - But, skip this step for the 'unknown publisher'.
    # !!! Why?   Seems like I _want_ to make entries for pub0, too.
    #
    if ($saleIDs) {
        foreach my $saleID (@$saleIDs) {

            my $mi = RPS::DB::Item::SaleRunMap->Lookup(
                sale_id           => $saleID,
                run_id            => $gRunID,
                run_type          => RPS::DB::Item::SaleRunMap::kRunTypeMechanical,
                statement_item_id => $statementItem->mechanical_statement_item_id,
                status            => RPS::DB::Item::SaleRunMap::kStatusPaid,
            );

            if ( !$mi ) {
                my $mi = RPS::DB::Item::SaleRunMap->Create(
                    sale_id           => $saleID,
                    run_id            => $gRunID,
                    run_type          => RPS::DB::Item::SaleRunMap::kRunTypeMechanical,
                    statement_item_id => $statementItem->mechanical_statement_item_id,
                    status            => RPS::DB::Item::SaleRunMap::kStatusPaid,
                );
                $mi->save();
            }
        }
    }

    return $statementItem;
}

sub _calculateEffectivePerTrackRate {
    my ( $albumID, $issueStatRateID, $saleStatRateID, $controlledComp, $ccTrackLicense ) = @_;

    assert($controlledComp);

    my $regionID = $ccTrackLicense->region_id;

    _report(
"\n\n<_calculateEffectivePerTrackRate>_calculateEffectivePerTrackRate: albumID $albumID regionID $regionID issueStatRateID $issueStatRateID saleStatRateID $saleStatRateID controlledComp "
          . Dumper($controlledComp),
        3
    );

    # !!! Use the database, not the xml classes.
    #
    my $tracks = RPS::DB::Item::Track->GetTracksByAlbumID($albumID);

    # (XXX) Prune out the bonus tracks - They are ignored in this context.
    #

    #    my $numTracks = $tracks->size();
    #    _report(" numTracks $numTracks",2);

    my @trackData;

    my $totalControlledShare = 0;
    my $sum                  = 0;
    my $outsideRate          = 0;
    my $numTracks;
    while ( $tracks->hasNext ) {
        my $track = $tracks->next();

        _report( "  looking at track :" . $track->track_id . " : " . $track->title, 3 );

        # (XXX) This will be a bit of a hack, but...
        # Need to skip bonus tracks.
        # I don't know, just by looking at the track, whether it's a bonus track.
        # Even if I had the product_id, I still would not really know, 'cause the
        # product might be a track product.
        #
        # So, this is a little goofy, but I am going to call a track a bonus track if
        # it _ever_ appears as a bonus track in the product_track table.
        #
        if ( RPS::DB::Item::ProductTrack->TrackIsBonusTrack( $track->track_id ) ) {
            _report( "  track is a bonus track, skipping", 3 );
            next;
        }

        # Need to get the duration of each track, just in case the rate basis is full...
        #
        my $masterID = $track->master_id;
        croak "!!! Track has no master, cannot determine duration" unless $masterID;
        my $masterData = RPS::DB::Item::Master->Lookup( master_id => $masterID );
        my $duration = $masterData->duration;

        # Get the theoretical rate for this track
        #
        # JPK - Note that I am calculating this with a 100% share.
        # I will apply the real percent later.
        #
        # This value is added to the 'albumPool'
        #
        my ($trackRate) = _calculateEffectiveRate(
            $issueStatRateID,           $saleStatRateID,             $controlledComp->rate_basis,
            $controlledComp->rate_type, $controlledComp->penny_rate, $controlledComp->rate_percentage,
            $duration,                  100,                         $controlledComp->lock_date
        );

        _report( " calculated max trackRate (with duration of $duration) = $trackRate", 3 );

        my $hasPublicDomainFlag = 0;
        my $trackShare          = 0;

        # !!! I believe that _this_ is where I need to take region into account
        #
        my $ccStartDate = $ccTrackLicense->term_start;
        my $ccEndDate   = $ccTrackLicense->term_end;
        my $trackLicenseList;
        if ( $ccStartDate && $ccEndDate ) {
            $trackLicenseList = RPS::DB::Item::TrackLicense->GetLicensesByTrackIDAndDate( $track->track_id, $ccStartDate, $ccEndDate );
        } else {
            $trackLicenseList = RPS::DB::Item::TrackLicense->GetLicensesByTrackID( $track->track_id );
        }
        while ( my $trackLicense = $trackLicenseList->next() ) {

            # Skip inactive licenses.
            #
            if ( $trackLicense->inactive() ) {
                _report( " ... skipping trackLicense " . $trackLicense->track_license_id . " : inactive", 2 );
                next;
            }

            # Skip public domain licenses.
            # We _might_ skip the whole track, so we set a flag to remind
            # us we saw a public domain license.
            #
            if ( RPS::DB::Item::TrackLicense::kPublicDomain eq $trackLicense->type ) {
                _report( " -- this is a public domain license, not going to calculate rate", 2 );
                $hasPublicDomainFlag = 1;
                next;
            }

            # !!! So, we'll try simply skipping over track licenses that don't fall in exactly the same
            # !!! region.  Not sure if that is the correct logic or not.
            #
            if ( $trackLicense->region_id != $regionID ) {
                _report( " ... skipping trackLicense " . $trackLicense->track_license_id . " : not in region $regionID", 2 );
                next;
            }
            $trackShare += $trackLicense->share;
            _report( " trackShare for track license " . $trackLicense->track_license_id . " = $trackShare", 2 );

            # If this is a controlled comp, we just need to note the share %.
            # Don't need to calculate a rate - it gets a share of what's left over.
            #
            if ( RPS::DB::Item::TrackLicense::kControlledComposition eq $trackLicense->type ) {
                _report( " -- this is a controll comp license, adding share of " . $trackLicense->share . " to controlled share", 2 );
                $totalControlledShare += $trackLicense->share;
                next;
            }

            # It's a normal track license, so calculate it's rate.
            # This rate will end up getting subtracted from the album rate.
            #
            my ($tempRate) = _calculateEffectiveRate(
                $issueStatRateID,         $saleStatRateID,           $trackLicense->rate_basis,
                $trackLicense->rate_type, $trackLicense->penny_rate, $trackLicense->rate_percentage,
                $duration,                $trackLicense->share,      $trackLicense->lock_date,
            );

            $outsideRate += $tempRate;
            _report( "  effective rate of $outsideRate for this track license: " . Dumper($trackLicense) . " outsideRate = $outsideRate",
                2 );
        }

        if ( $trackShare >= 100 && $hasPublicDomainFlag ) {
            _report( "  track is 100% public domain, skipping it", 2 );
            next;
        }

        # Now that we've looked at all the track licenses, we know whether we're
        # going to 'count' this track.
        #
        $sum += $trackRate;
        $numTracks++;
        _report( "using this track in ccomp calulations. sum=$sum, trackRate=$trackRate, numTracks=$numTracks", 3 );

        # Any unallocated shares are 'assigned' to the unknown publisher, and will
        # have their rate calculated at the maximum possible rate.
        #
        _report( "  trackShare $trackShare", 2 );

        if ( $trackShare < 100 ) {
            my $remainingShare = 100 - $trackShare;

            my ($tempRate) = _calculateEffectiveRate(
                $issueStatRateID, $saleStatRateID,
                RPS::DB::Item::TrackLicense::kRateBasisSale,
                RPS::DB::Item::TrackLicense::kRateTypeFull,
                0, 100, $duration, $remainingShare, 0
            );

            $outsideRate += $tempRate;

            _report( "  remainingShare = $remainingShare.  outsideRate = $outsideRate", 2 );
        }
    }

    # Do the final calculations.
    #
    my $averagePerTrackRate = ( $sum / $numTracks );
    _report( "averagePerTrackRate $averagePerTrackRate = albumPool $sum / numTracks $numTracks", 3 );

    my $albumRateNumTracks = ( $controlledComp->track_cap < $numTracks ? $controlledComp->track_cap : $numTracks );
    _report( "using $albumRateNumTracks as num tracks", 3 );

    my $nominalAlbumRate   = $averagePerTrackRate * $albumRateNumTracks;
    my $effectiveAlbumRate = $nominalAlbumRate - $outsideRate;
    _report( "effective rate $effectiveAlbumRate = nominalRate $nominalAlbumRate - outsideRate $outsideRate", 3 );
    _report( "totalControlledShare: $totalControlledShare",                                                   3 );

    my $returnRate = ( $effectiveAlbumRate / ( $totalControlledShare / 100 ) );
    _report( "Returning rate $returnRate", 3 );

    return $returnRate;
}

sub _getTracksFromProduct {
    my ($product) = @_;

    my @tracks;

    if ( RPS::DB::Item::Product::kProductTypeDigitalTrack == $product->product_type_id ) {
        my $trackDBItem = RPS::DB::Item::Track->Lookup( track_id => $product->asset_id );
        my $track = _getTrack( $trackDBItem->track_id );
        if ( !$track->mechanical_exempt ) {
            push @tracks, $trackDBItem;
        }
    } else {

        # (XXX) Make sure this interface takes bonus tracks into account.
        # jpk - seems to.
        #
        my $collection = RPS::DB::Item::ProductTrack->GetTracksByProductID( $product->product_id );
        while ( $collection->hasNext() ) {
            my $trackDBItem = $collection->next();
            my $track       = _getTrack( $trackDBItem->track_id );
            if ( !$track ) {
                _report( "ERROR - bad track_id in product_track record: " . Dumper($trackDBItem) );
                next;
            }
            if ( !$track->mechanical_exempt ) {
                push @tracks, $trackDBItem;
            }
        }
    }

    return \@tracks;
}

sub _report {
    my ( $string, $verbosity ) = @_;
    $verbosity = 1 unless defined $verbosity;

    if ( $gVerbosityLevel >= $verbosity ) {
        print STDERR $string . "\n";
    }
}

sub _getPreviousSaleStatementItems {
    my ( $saleID, $saleStatRateID, $issueStatRateID ) = @_;

    my $hashRef = {};

    my $collection = RPS::DB::Item::SaleRunMap->GetPaidMechanicalBySaleID($saleID);

    #    my $collection = RPS::DB::Item::SaleMechanicalStatementItemMap->GetBySaleID($saleID);

    while ( $collection->hasNext() ) {
        my $mapEntry = $collection->next();

        my $statementItem = RPS::DB::Item::MechanicalStatementItem->Lookup( mechanical_statement_item_id => $mapEntry->statement_item_id );
        next unless $statementItem;

        # Only consider statement items for committed or closed runs.
        # This will allow us to have more than 1 uncommited run at a time, which
        # is good for auditing purposes.
        #
        next if ( !_statementIsOnCommittedRun( $statementItem->mechanical_statement_id ) );

        my $trackLicenseID = $statementItem->track_license_id;

        # !!! Just for kicks, let's try skipping sales associated with pub0.
        #
        my $trackLicense = RPS::DB::Item::TrackLicense->Lookup( track_license_id => $trackLicenseID );
        next if ( 0 == $trackLicense->publisher_id );

        if ( $hashRef->{$trackLicenseID} ) {
            _report( "ERROR - unexpectedly encountered more that 1 statement for " . "sale_id $saleID, track_license_id $trackLicenseID" );
        }
        $hashRef->{ $statementItem->track_license_id } = $statementItem;
    }

    return $hashRef;
}

my $gCommittedStatements;

sub _statementIsOnCommittedRun {
    my ($mechanicalStatementID) = @_;

    if ( !$gCommittedStatements ) {
        _report( "building a hash of committed statement ids", 3 );
        $gCommittedStatements = RPS::DB::Item::MechanicalStatement->GetIDSFromCommittedRuns();
    }

    return $gCommittedStatements->{$mechanicalStatementID};
}

sub _processControlledCompAdjustments {
    my ( $adjustmentState, $startDate, $endDate ) = @_;

    # !!! So, I now have a structure that contains all the statement items
    #     that have been affected by the addition of a new license.
    #
    #     We have also already gone ahead and created statements for controlled comps.
    #     However, those statements were for 'new' sales.
    #
    #     There is probably a way to combine the two processes, but for now I'm going
    #     to just use brute force.

    _report( Dumper($adjustmentState), 3 );

    foreach my $trackLicenseID ( keys %$adjustmentState ) {
        _report( " CComp adjustment for trackLicense $trackLicenseID", 2 );

        my $statementItemList = $adjustmentState->{$trackLicenseID};
        my $trackLicense      = RPS::DB::Item::TrackLicense->Lookup( track_license_id => $trackLicenseID );
        my $payorID           = $trackLicense->payor_id;

        my $publisherID   = $trackLicense->publisher_id;
        my $publisherData = _getPublisher($publisherID);
        if ( $publisherData && RPS::DB::Item::Publisher::kStatusInactive == $publisherData->status() ) {
            _report( "  this publisher is inactive, skipping", 3 );
            next;
        }

        # !!! Sanity check
        #
        die "ERROR - expected a controlled comp license"
          unless ( RPS::DB::Item::TrackLicense::kControlledComposition == $trackLicense->type );

        my $trackID = $trackLicense->track_id;
        my $album   = RPS::DB::Item::Album->GetByTrackID($trackID);
        my $albumID = $album->album_id;
        my $controlledComp =
          RPS::DB::Item::ControlledComposition->Lookup( controlled_composition_id => $trackLicense->controlled_composition_id );

        foreach my $statementItem (@$statementItemList) {

            # Based on this data, I should be able to create an adjustment item.
            #
            # First, though, I need to see if there are any previous adjustments to
            # this statement item.
            #

            # !!! - Should this be total, or amount_paid?
            #
            #            my $previousTotal = $statementItem->total;
            my $previousTotal = $statementItem->amount_paid;

            my $statementItemID = $statementItem->mechanical_statement_item_id;
            _report( "   adjusting statement item $statementItemID", 2 );

            my $lastAdjustmentID = 0;
            my $lastAdjustment   = RPS::DB::Item::MechanicalStatementAdjustmentItem->GetLastStatementAdjustment($statementItemID);
            if ($lastAdjustment) {
                $previousTotal    = $lastAdjustment->new_total;
                $lastAdjustmentID = $lastAdjustment->mechanical_statement_adjustment_item_id;
                _report( "   previous adjustment id: $lastAdjustmentID", 2 );
            }

            # Why bother adjusting $0.00 amounts?
            #
            next unless $previousTotal > 0;

            # Re-calculate the new effective rate.
            #
            #            my $issueStatRateID = $statementItem->issue_stat_rate_id;
            my $issueStatRateID = 16;
            my $saleStatRateID  = $statementItem->sale_stat_rate_id;
            my $effectivePerTrackRate =
              _calculateEffectivePerTrackRate( $albumID, $issueStatRateID, $saleStatRateID, $controlledComp, $trackLicense );

            # Calculate the new total.
            #
            my $share = $trackLicense->share;
            my $units = $statementItem->net_units;
            #
            # net_units are the units after reserves were taken.

            my $effectiveRate = ( $effectivePerTrackRate * ( $share / 100 ) );

            # !!! Let's try leveraging the sprintf to round for us.
            # !!! We currently are accurate to 4 decimal places.
            #
            #            my $total = $units * $effectiveRate;
            #            my $total = sprintf("%.4f", ($units * $effectiveRate));
            my $total = Common::RSMath::round( $units * $effectiveRate, 4 );

            # Calculate the adjustment amount.
            # !!! Is there _ever_ a reason why the adjustmentAmount should be < 0?
            #
            # !!! This math is suspect
            #     - it's possible for results that should yield $adjustmentAmount == 0 to
            #       instead give _very very small_ values.
            #
            _report( "   calculation factors: share = $share  units = $units  perTrackRate = $effectivePerTrackRate", 3 );
            _report( "     effectiveRate = effectivePerTrackRate * (share / 100) == $effectiveRate",                  3 );
            my $adjustmentAmount = $total - $previousTotal;
            _report( "   old total: $previousTotal  new total: $total", 2 );

            if ( $adjustmentAmount < 0.01 ) {

                # Having the adjustment == 0 is not _that_ odd of an occurance.
                # I believe the very first time we run mechanicals we should see this a lot.
                #
                # Going to extend this to limit adjustments to at least 1 cent.
                #
                warn "adjustment on track license $trackLicenseID would yield amount of $adjustmentAmount - skipping!";
                next;
            }

            # Get the statement that this adjustment will appear on.
            # (This is _not_ the original statement for the original statement item).
            #
            # !!! Hey now - if this is not a direct license, I need to use the admin/agent publisherID
            #

            if ( !$trackLicense->publisher_direct ) {

                my $agentID = $publisherData->agent_id;
                my $adminID = $publisherData->admin_id;
                if ($agentID) {
                    $publisherID = $agentID;
                    _report( "    reporting on agent $publisherID statement", 2 );
                } elsif ($adminID) {
                    $publisherID = $adminID;
                    _report( "    reporting on admin $publisherID statement", 2 );
                }
            }

            my $newStatement = _getStatement( $payorID, $publisherID, $startDate, $endDate );

            # Now we can create the adjustment item.
            #
            my $newStatementID = $newStatement->mechanical_statement_id;
            my $adjustmentItem = RPS::DB::Item::MechanicalStatementAdjustmentItem->Create(
                mechanical_statement_id                          => $newStatementID,
                track_license_id                                 => $trackLicenseID,
                original_mechanical_statement_item_id            => $statementItemID,
                previous_mechanical_statement_adjustment_item_id => $lastAdjustmentID,
                amount                                           => $adjustmentAmount,
                new_total                                        => $total,
                adjustment_type_code => RPS::DB::Item::MechanicalStatementAdjustmentItem::kAdjustmentTypeCCompNewLicense,
            );
            $adjustmentItem->save();

            _report( "NEW ADJUSTMENT ITEM ID: " . $adjustmentItem->mechanical_statement_adjustment_item_id, 3 );
        }
    }
}

sub _findLicensesToAdjust {
    my ( $startDate, $endDate, $payorIDMap ) = @_;

    # This means find all new track_license entries.
    # Fetch the album associated with that track.
    # See if there is a controlled comp associated with it.
    # If there is, we want to make a list of affected track (or track_licenses),
    # so that we can flag sales associated with them as needing adjustment.

    my %licenseMap;

    my $newTrackLicenses = RPS::DB::Item::TrackLicense->GetNewTrackLicenses();
    while ( $newTrackLicenses->hasNext() ) {
        my $trackLicense = $newTrackLicenses->next();
        next if $trackLicense->inactive();

        # Skip track licenses that belongs to inactive publishers
        #
        my $publisher = _getPublisher( $trackLicense->publisher_id );
        next if ( $publisher && RPS::DB::Item::Publisher::kStatusInactive == $publisher->status() );

        # Skip track licenses on inactive albums
        #
        my $track = RPS::DB::Item::Track->Lookup( track_id => $trackLicense->track_id );
        next unless $track;

        my $trackID = $trackLicense->track_id;
        my $albumID = $track->album_id;
        next unless $albumID;
        my $album = RPS::DB::Item::Album->Lookup( album_id => $albumID );
        if ( $album->inactive() || $album->status() == 0 ) {
            next;
        }

        # I think I can skip controlled comp licenses.
        # (XXX) - Can I?  Can additional controlled comps be added after a run?
        #         What does that really mean, if anything?
        #
        next if ( RPS::DB::Item::TrackLicense::kControlledComposition == $trackLicense->type );

        # (XXX) ccomp can be associated with an individual track, too, not just
        # a whole album.

        # We want to get all the controlled comp trackLicense entries for this album.
        # (if any)
        #
        my $affectedLicenses = RPS::DB::Item::TrackLicense->GetCCompTrackLicensesForAlbum($albumID);
        while ( $affectedLicenses->hasNext() ) {
            my $affectedTrackLicense = $affectedLicenses->next();

            # Skip licenses that don't belong to the request payor ids
            #
            my $payorID = $affectedTrackLicense->payor_id;
            if ( !$payorIDMap->{$payorID} ) {
                next;
            }

            $licenseMap{ $affectedTrackLicense->track_license_id } = $affectedTrackLicense;
        }
    }

    return \%licenseMap;
}

sub _populateCache {
    my ($id) = @_;

    _report("populating XML Cache");

    # Get the statements associated with this run.
    #
    my $statementList = RPS::DB::Item::MechanicalStatement->GetByMechanicalRunID($id);
    while ( $statementList->hasNext() ) {
        my $statement   = $statementList->next();
        my $statementID = $statement->mechanical_statement_id;

        my $publisher = RPS::DB::Item::Publisher->Lookup( publisher_id => $statement->publisher_id );
        my $pubNameLC = lc( $publisher->publisher_name );
        next unless $pubNameLC =~ /harry fox/;

        #        next unless ('harry fox agency' eq lc($publisher->publisher_name));
        #next unless (1 == $publisher->publisher_id);

        print "   populating cache for statement $statementID\n";

        my $statementXML = RPS::Statement::Mechanical::StatementDisplay->new( mechanicalStatementID => $statementID );
        RPS::XMLCache::Store( $statementXML, 'RPS', 'Statement', 'Command', 'ShowMechanical', $statementID );

        # Cache the hfa statement if this publisher is an agent.
        #
        if ( $publisher->is_agency || $publisher->is_admin ) {
            print "   publisher is admin/agent, populating additional files\n";

# Get a list of all the publisher ids that appear on this statement.
#
#            my $publisherItems = RPS::DB::Item::MechanicalStatementPublisher->GetByMechanicalStatementID($statementID);
#            while (my $pubItem = $publisherItems->next())
#            {
#                my $focusID = $pubItem->publisher_id;
#                $statementXML = RPS::Statement::Mechanical::StatementDisplay->new(mechanicalStatementID => $statementID,
#                 focusPublisherID => $focusID);
#                RPS::XMLCache::Store($statementXML, 'RPS','Statement','Command','ShowMechanical_focusPublisherID_' . $focusID, $statementID);
#            }
#
#
            $statementXML = RPS::Statement::Mechanical::StatementDisplay->new( mechanicalStatementID => $statementID, hfa => 1 );
            RPS::XMLCache::Store( $statementXML, 'RPS', 'Statement', 'Command', 'ShowMechanical_hfa', $statementID );
        }
    }
}

# Going to fetch _all_ track licenses in the requested date range at once.
# Then we'll arrange them by track id and product type
# !!! This caching?  Probably not such a great idea - start and end date are going to
# !!! be variable now (tied to the sale record, not the command-line).
#
sub new_getTrackLicenses {
    my ( $trackID, $startDate, $endDate, $productTypeID ) = @_;

    # jpk - I am 'collapsing' the possible range of product type ids.
    # 1 and 2 will be equated with 1, 3 and 4 with 3.
    #
    $productTypeID = 1 if ( $productTypeID == 2 );
    $productTypeID = 3 if ( $productTypeID == 4 );

    if ( !$gTrackLicenseMap ) {
        _report( "Caching track licenses..", 2 );

        $gTrackLicenseMap = {};

        # Fetch all track_licenses in the requested date range.
        #
        my $c = RPS::DB::Item::TrackLicense->GetIssuedLicensesByDate( $startDate, $endDate );

        my %trackPubTypeMap;

        while ( my $tl = $c->next() ) {
            my $tlTrackID = $tl->track_id;

            #            _report("  track_id $tlTrackID", 2);

            # We want to arrange them by product type.
            #
            foreach my $sortProductTypeID ( 0, 1, 3 ) {
                next if ( !_productTypeMatches( $sortProductTypeID, $tl->product_type_id ) );

                # The goal is to end up with one license per publisher per track.
                # If we have 2 or more, then 1 of them must go.
                # If we can't unambiguously determine which is a better fit...
                # well, then we freak out.
                #
                my $pubID = $tl->publisher_id;

                if ( exists $trackPubTypeMap{$tlTrackID}{$pubID}{$sortProductTypeID} ) {
                    _report( "    --- more than one? ", 2 );
                    my $otherTLArray = $trackPubTypeMap{$tlTrackID}{$pubID}{$sortProductTypeID};

                    # Don't need to look at everything - just the first one.
                    my $otherTL = $otherTLArray->[0];

                    if ( $otherTL->product_type_id == $tl->product_type_id ) {
                        _report( "       --- type is the same ", 2 );

                        # Add this track license to the ones we've seen before.
                        #
                        push @{ $trackPubTypeMap{$tlTrackID}{$pubID}{$sortProductTypeID} }, $tl;
                    } elsif ( $otherTL->product_type_id < $tl->product_type_id ) {
                        _report( "       --- new one more specific, replacing", 2 );

                        # This track license is a better match than the ones we seen
                        # before, so replace them.
                        #
                        $trackPubTypeMap{$tlTrackID}{$pubID}{$sortProductTypeID} = [$tl];
                    } else {
                        _report( "       --- old ones more specific, skipping", 2 );

                        # The track licenses we've seen before are a better match, we
                        # can skip this one.
                        #
                        next;
                    }
                } else {
                    $trackPubTypeMap{$tlTrackID}{$pubID}{$sortProductTypeID} = [$tl];
                }
            }
        }

        # Now convert this big ole' hash into a set of arrays.
        #
        _report( "  converting to arrays", 2 );
        foreach my $hashTrackID ( keys %trackPubTypeMap ) {
            my $pubHash = $trackPubTypeMap{$hashTrackID};
            foreach my $hashPubID ( keys %$pubHash ) {
                my $typeHash = $pubHash->{$hashPubID};
                foreach my $hashTypeID ( keys %$typeHash ) {
                    my $hashTLArray = $typeHash->{$hashTypeID};
                    foreach my $hashTL (@$hashTLArray) {
                        push @{ $gTrackLicenseMap->{$hashTrackID}{$hashTypeID} }, $hashTL;
                    }
                }
            }
        }
    }
    _report( "  returning track licenses", 2 );

    _report(
        "  track licenses for trackID $trackID productTypeID $productTypeID = " . Dumper( $gTrackLicenseMap->{$trackID}{$productTypeID} ),
        3 );

    return $gTrackLicenseMap->{$trackID}{$productTypeID};
}

sub _getTrackLicenses {
    my ( $trackID, $startDate, $endDate, $productTypeID, $countryCode ) = @_;

    _report( "_getTrackLicenses: trackID $trackID  startDate $startDate  endDate $endDate  productTypeID $productTypeID", 4 );

    # (XXX) one 'last' schema change - now everythng is flat, and simply stored
    # (XXX) in the track license table.

    # Get the track_licenses for this track that fit the date range.
    #
    my $c = RPS::DB::Item::TrackLicense->GetLicensesByTrackIDAndDate( $trackID, $startDate, $endDate );

    # Iterate over the track_licenses, and keep those that fit the requested product_type best.
    # jpk - at this point, we have limited choices here.
    # All Product == NULL
    # LP RPS::DB::Item::Product::kProductTypeLP
    # CD kProductTypeCD
    # D  kProductTypeDigital
    # DT kProductTypeDigital  (in other words, incoming types of 3 or 4 match track licenses of type '3');

    my %pubIDMap;
    while ( my $tl = $c->next() ) {
        _report( "  evaluating track license " . Dumper($tl), 4 );
        if ( $tl->inactive() ) {
            _report( "  skipping -- inactive", 3 );
            next;
        }

        my $regionID = $tl->region_id;

        # Skip this track_license if it doesn't match at all.
        #
        if ( !_productTypeMatches( $productTypeID, $tl->product_type_id ) ) {
            _report( "  skipping because productTypeMatches failed - passed in product type id $productTypeID", 3 );
            next;
        }

        # Skip this license if the region doesn't match the country code.
        #
        if ( RPS::DB::Item::TrackLicense::kPublicDomain ne $tl->type ) {
            if ( !_regionMatchesCountryCode( $countryCode, $regionID ) ) {
                _report( "  skipping because countryCode $countryCode did not match region $regionID", 3 );
                next;
            }
        }

        # The goal is to end up with one license per publisher.
        # (XXX) Per _region_...
        #
        # If we have 2 or more, then 1 of them must go.
        # If we can't unambiguously determine which is a better fit...
        # well, then we freak out.
        #
        my $pubID = $tl->publisher_id;

        # Skip inactive publisher licenses
        #
        my $pub = _getPublisher($pubID);
        if ( $pub && RPS::DB::Item::Publisher::kStatusInactive == $pub->status() ) {
            _report( "this track licenses belongs to an inactive publisher, skipping", 3 );
            next;
        }

        # Skip licenses on inactive albums
        #
        my $track = RPS::DB::Item::Track->Lookup( track_id => $tl->track_id );
        my $album = RPS::DB::Item::Album->Lookup( album_id => $track->album_id );
        if ( $album->inactive() || $album->status() == 0 ) {
            _report( "track license " . $tl->track_license_id . " is on an inactive album, skipping", 3 );
            next;
        }

        if ( exists $pubIDMap{$pubID} ) {
            my $otherTL = $pubIDMap{$pubID};

            # sanity check - this ought not to happen...
            # (!!!) Going to keep track of 'ambiguous' warnings, to avoid repeating myself.
            #
            if ( $tl->product_type_id == $otherTL->product_type_id ) {

          #                die("ERROR!  track_licenses " . $tl->track_license_id . " and " . $otherTL->track_license_id . " are ambiguous");
                _report( "ERROR!  track_licenses " . $tl->track_license_id . " and " . $otherTL->track_license_id . " are ambiguous" );
                next;
            }

            # more specific beats less specific.
            # jpk - at this point, the type will either be undef, or a specific
            # value.  So this is currently an easy determination to make... this may
            # get more complicated if we change the product type encoding scheme.
            #
            # !!! Now with the 'all physical' license type with an id of 255
            if ( $otherTL->product_type_id > 0 && $otherTL->product_type_id != 255 ) {

                # We've already got a 'specific' type, so it wins.
                #
                next;
            } elsif ( $otherTL->product_type_id == 255 && !$tl->product_type_id ) {

                # We've got the all physical products license, so the 'all products' license loses...
                #
                next;
            }

        }

        $pubIDMap{$pubID} = $tl;
    }

    # Collapse the map into a single array.
    #
    my @outList;
    foreach my $pubIDKey ( keys %pubIDMap ) {
        push @outList, $pubIDMap{$pubIDKey};
    }

    return \@outList;
}

# (XXX) I dislike this.
#
sub _productTypeMatches {
    my ( $productTypeID, $licenseProductTypeID ) = @_;

    return 1 if !$licenseProductTypeID;
    return 1 if !$productTypeID;

    return 1
      if (
        255 == $licenseProductTypeID
        && (   $productTypeID != RPS::DB::Item::Product::kProductTypeDigital
            && $productTypeID != RPS::DB::Item::Product::kProductTypeDigitalTrack )
      );

    # Apply cd licenses to lps
    #
    return 1 if ( $productTypeID == 1 && $licenseProductTypeID == 2 );

    # Apply digital album licenses to digital tracks
    #
    return 1 if ( $productTypeID == 4 && $licenseProductTypeID == 3 );

    return ( $productTypeID == $licenseProductTypeID );
}

sub _getProduct {
    my ($productID) = @_;

    if ( !$gProductMap ) {
        _report( "Caching all products...", 2 );

        $gProductMap = {};
        my $productList = RPS::DB::Item::Product->GetAll();
        while ( my $product = $productList->next() ) {
            $gProductMap->{ $product->product_id } = $product;
        }
    }

    return $gProductMap->{$productID};
}

sub _getProductType {
    my ($productTypeID) = @_;

    if ( !$gProductTypeMap ) {
        _report( "Caching all product types...", 2 );

        $gProductTypeMap = {};
        my $productTypeList = RPS::DB::Item::ProductType->GetAll();
        while ( my $productType = $productTypeList->next() ) {
            $gProductTypeMap->{ $productType->product_type_id } = $productType;
        }
    }

    return $gProductTypeMap->{$productTypeID};
}

sub _getTrack {
    my ($trackID) = @_;

    if ( !$gTrackMap ) {
        _report( "Caching all tracks...", 2 );

        $gTrackMap = {};
        my $trackList = RPS::DB::Item::Track->GetAll();
        while ( my $track = $trackList->next() ) {
            $gTrackMap->{ $track->track_id } = $track;
        }
    }
    return $gTrackMap->{$trackID};
}

sub _getAlbum {
    my ($albumID) = @_;

    if ( !$gAlbumMap ) {
        _report( "Caching all albums...", 2 );

        $gAlbumMap = {};
        my $albumList = RPS::DB::Item::Album->Get();
        while ( my $album = $albumList->next() ) {
            $gAlbumMap->{ $album->album_id } = $album;
        }
    }
    return $gAlbumMap->{$albumID};
}

sub _getLabel {
    my ($labelID) = @_;

    if ( !$gLabelMap ) {
        _report( "Caching all labels...", 2 );

        $gLabelMap = {};
        my $labelList = RPS::DB::Item::Label->GetAll();
        while ( my $label = $labelList->next() ) {
            $gLabelMap->{ $label->label_id } = $label;
        }
    }

    return $gLabelMap->{$labelID};
}

sub _getArtist {
    my ($artistID) = @_;

    if ( !$gArtistMap ) {
        _report( "Caching all artist...", 2 );

        $gArtistMap = {};
        my $artistList = RPS::DB::Item::Artist->GetAll();
        while ( my $artist = $artistList->next() ) {
            $gArtistMap->{ $artist->artist_id } = $artist;
        }
    }
    return $gArtistMap->{$artistID};
}

sub _getMaster {
    my ($masterID) = @_;

    if ( !$gMasterMap ) {
        _report( "Caching all masters...", 2 );

        $gMasterMap = {};
        my $masterList = RPS::DB::Item::Master->Get();
        while ( my $master = $masterList->next() ) {
            $gMasterMap->{ $master->master_id } = $master;
        }
    }

    return $gMasterMap->{$masterID};
}

sub _getSong {
    my ($songID) = @_;

    if ( !$gSongMap ) {
        _report( "Caching all songs...", 2 );

        $gSongMap = {};
        my $songList = RPS::DB::Item::Song->GetAll();
        while ( my $song = $songList->next() ) {
            $gSongMap->{ $song->song_id } = $song;
        }
    }

    return $gSongMap->{$songID};
}

sub _getSongByTrackID {
    my ($trackID) = @_;

    my $track = _getTrack($trackID);
    return undef unless $track;

    my $master = _getMaster( $track->master_id );
    return undef unless $master;

    return _getSong( $master->song_id );
}

sub _getPublisher {
    my ($publisherID) = @_;

    if ( !$gPublisherMap ) {
        _report( "Caching all publishers...", 2 );
        my $publisherList = RPS::DB::Item::Publisher->GetAll();
        while ( my $publisher = $publisherList->next() ) {
            $gPublisherMap->{ $publisher->publisher_id } = $publisher;
        }
    }

    return $gPublisherMap->{$publisherID};
}

my $gServiceMap;

sub _serviceIDToName {
    my ($serviceID) = @_;

    if ( !$gServiceMap ) {
        _report( "Caching services...", 2 );
        my $serviceList = RPS::DB::Item::Service->GetAll();
        while ( my $service = $serviceList->next() ) {
            $gServiceMap->{ $service->service_id } = uc( $service->service_name_clean );
        }
    }

    #    return $gServiceMap->{$serviceID};
    # JPK - Supressing this for now...
    return "";
}

my $gFileServiceMap;

sub _fileIDToService {
    my ($fileID) = @_;

    if ( !$gFileServiceMap ) {
        _report( "Caching file service ids...", 2 );
        my $fileList = Raptor::DB::Item::File->GetAll();
        while ( my $file = $fileList->next() ) {
            $gFileServiceMap->{ $file->file_id } = $file->service_id;
        }
    }

    return $gFileServiceMap->{$fileID};
}

sub _getUPCAndISRCFromProductID {
    my ($productID) = @_;

    my $upc;
    my $isrc;

    # Fetch the product first.
    #
    my $product = _getProduct($productID);
    if ($product) {
        $upc = $product->upc_ean;

        if ( RPS::DB::Item::Product::kProductTypeDigitalTrack == $product->product_type_id ) {

            # We don't have UPCs for track products.
            # So we will have to fetch it from the album.
            # ??? Will digital album products have different UPC from physical track products?
            #
            my $track = _getTrack( $product->asset_id );
            my $album = _getAlbum( $track->album_id );
            if ( $album && !$upc ) {
                my $products =
                  RPS::DB::Item::Product->GetProductsByAlbumID( $album->album_id, RPS::DB::Item::Product::kProductTypeDigital );
                my $albumProduct = $products->next();
                if ($albumProduct) {
                    $upc = $albumProduct->upc_ean;
                }
            }
            my $master = _getMaster( $track->master_id );
            if ($master) {
                $isrc = $master->isrc;
            }
        }

        return ( $upc, $isrc );
    }
}

sub _calculateHFAReportingPeriod {
    my ( $startDate, $endDate, $issueStatRateID, $saleStatRateID, $trackLicense ) = @_;

    # The default we'll use is just the provided startDate and endDate
    #

    if ( $issueStatRateID && $saleStatRateID ) {
        my $statRates     = _getStatRateTable();
        my $appliedRateID = $saleStatRateID;

        # use the stat date's effective date as our start range if it is later
        # that our run's start date.  jpk - Remember that we've already
        # taken the sale date into account before choosing the stat rate id.
        # JPK - turn this logic around...
        #
        if ( $statRates->{$appliedRateID}->date_effective lt $startDate ) {
            $startDate = $statRates->{$appliedRateID}->date_effective;
            _report( "  using $startDate for startDate when calculating HFA reporting period - stat rate id $appliedRateID", 2 );
        }

        # See if there is another stat rate after this one in the table.
        # If there is, then we may need to use it's effective date to cap our
        # reporting period.
        #
        if ( exists $statRates->{ $appliedRateID + 1 } ) {
            if ( $statRates->{ $appliedRateID + 1 }->date_effective lt $endDate ) {
                $endDate = $statRates->{ $appliedRateID + 1 }->date_effective;

                # Need to subtract 1 day, so that the endDate is in the previous month.
                #
                my ( $yy, $mm, $dd ) = split( '-', $endDate );
                my ( $ny, $nm, $nd ) = Date::Calc::Add_Delta_Days( $yy, $mm, $dd, -1 );
                $endDate = join( '-', $ny, $nm, $nd );
                _report( "  using $endDate for endDate when calculating HFA reporting period -stat rate id " . ( $appliedRateID + 1 ), 2 );
            }
        }
    }

    # jpk - At some point we'll have to contemplate license term_start and term_end, too.
    #

    _report( "  HFA reporting period startDate $startDate  endDate $endDate", 2 );

    my ( $startYear, $startMonth, $startDay ) = split( '-', $startDate );
    my ( $endYear,   $endMonth,   $endDay )   = split( '-', $endDate );

    my ( $dy, $dm, $dd ) = Date::Calc::Delta_YMD( $startYear, $startMonth, $startDay, $endYear, $endMonth, $endDay );

    my $months = ( 12 * $dy ) + $dm;
    if ( $dd > 0 ) {
        $months++;
    }

    # Good ole' harry fox assumes the year is >= 2000.
    #
    my $period = sprintf( "%02d%02d%02d", $months, $endMonth, ( $endYear - 2000 ) );
    return $period;
}

sub _productIsDigital {
    my ($productID) = @_;

    my $product = _getProduct($productID);
    my $typeID  = $product->product_type_id;

    if (   RPS::DB::Item::Product::kProductTypeDigital == $typeID
        || RPS::DB::Item::Product::kProductTypeDigitalTrack == $typeID ) {
        return 1;
    }

    return 0;
}

sub _getControlledCompIDFromAlbumID {
    my ($albumID) = @_;

    my $albumContract = RPS::DB::Item::AlbumContract->GetCComp($albumID);
    return undef unless $albumContract;

    my $cc = RPS::DB::Item::ControlledComposition->GetByContractID( $albumContract->artist_contract_id );
    return $cc->controlled_composition_id;
}

sub _standardDeductions {
    my ( $units, $trackLicense ) = @_;

    #    $units = ceil($units * ($trackLicense->percentage_of_sales / 100 ));
    #    _report("    units after applying PercentageOfSales of " . $trackLicense->percentage_of_sales . " = $units", 2);

    my $percentOfSalesDeduction = 100 - $trackLicense->percentage_of_sales;
    $units = _applyDeduction( $units, $percentOfSalesDeduction );
    _report( "    units after applying PercentageOfSales deduction of " . $percentOfSalesDeduction . " = $units", 2 );

    $units = _applyDeduction( $units, $trackLicense->packaging_deduction );
    _report( "    units after applying PackagingDeduction of " . $trackLicense->packaging_deduction . " = $units", 2 );

    $units = _applyDeduction( $units, $trackLicense->free_goods );
    _report( "    units after applying FreeGoods of " . $trackLicense->free_goods . " = $units", 2 );

    $units = _applyDeduction( $units, $trackLicense->misc_deduction );
    _report( "    units after applying MiscDeduction of " . $trackLicense->misc_deduction . " = $units", 2 );

    return $units;
}

sub _roundRate {
    my ($rate) = @_;

    my $newRate = Common::RSMath::round( $rate, 4 );
    return $newRate;
}

sub _getAllPayorIDs {
    my @ids;
    my $payors = RPS::DB::Item::Payor->GetAll();
    while ( my $payor = $payors->next() ) {
        push @ids, $payor->payor_id;
    }

    return \@ids;
}

sub _productAlbumIsInactive {
    my ($product) = @_;

    my $albumID = $product->asset_id;

    if ( RPS::DB::Item::Product::kProductTypeDigitalTrack == $product->product_type_id ) {
        my $trackData = _getTrack( $product->asset_id );
        $albumID = $trackData->album_id;
    }

    my $album = _getAlbum($albumID);
    return $album->inactive;
}

sub _getControlledCompIDFromTrackID {
    my ($track_id) = @_;

    my $track = RPS::DB::Item::Track->Lookup( track_id => $track_id );
    my $albumID = $track->album_id;

    my $albumContract = RPS::DB::Item::AlbumContract->GetCComp($albumID);
    return undef unless $albumContract;

    my $cc = RPS::DB::Item::ControlledComposition->GetByContractID( $albumContract->artist_contract_id );
    return $cc->controlled_composition_id;
}

sub _regionMatchesCountryCode {
    my ( $countryCode, $regionID ) = @_;

    # region_id of 0?  that's a problem.  We don't allow
    # a '0' region in track licenses.
    #
    assert($regionID);

    # Pretty straight-forward - Just use the RegionCountryMap table
    #
    my $dbItem = RPS::DB::Item::RegionCountryMap->Lookup( country_code => $countryCode, region_id => $regionID );
    return ( defined $dbItem );
}

sub _getCorrectPublisherID {
    my ($trackLicense) = @_;

    my $publisherID = $trackLicense->publisher_id;

    # Check to see whether this track_license is tagged as 'publisher_direct'.
    # If it _is_, then we proceed as usual.
    # If _not_, then activity on this license is not reported on the publisher's
    # statement - instead, it gets reported on the Agent's statement.
    #
    if ( !$trackLicense->publisher_direct ) {
        my $publisherData = _getPublisher($publisherID);

        my $agentID = $publisherData->agent_id;
        my $adminID = $publisherData->admin_id;
        if ($agentID) {
            $publisherID = $agentID;
            _report( "    reporting on agent $publisherID statement", 2 );
        } elsif ($adminID) {
            $publisherID = $adminID;
            _report( "    reporting on admin $publisherID statement", 2 );
        }

        # JPK - Note that if the agentID was not set, then we essentially
        # ignore the publisherDirect flag (and treat it automatically as publisherDirect)
        #
    }

    return $publisherID;
}

sub _licenseIsValid {
    my ($trackLicense) = @_;

    # If this is a full-stat-rate license, but we don't have a duration, then
    # this is NOT a valid license.  Skip it.
    #
    if ( RPS::DB::Item::TrackLicense::kRateTypeFull == $trackLicense->rate_type ) {
        my $track = _getTrack( $trackLicense->track_id );

        my $masterID = $track->master_id;
        die "!!! Track has no master, cannot determine duration" unless $masterID;

        my $masterData = _getMaster($masterID);
        my $duration   = $masterData->duration;
        if ( !$duration ) {
            _report( "WARNING: TrackLicenseID " . $trackLicense->track_license_id . " is a full stat rate with no duration, skipping" );
            return 0;
        }
    }

    return 1;
}

# Putting a bit of abstraction around looking up individual track licenses.
# We need to be able to create licenses with 'pseudo-ids', so we'll be able to render
# licenses that have somehow changed or been deleted.
#
sub _getTrackLicense {
    my ($trackLicenseID) = @_;

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