#------------------------------------------------------------
# Copyright (C) 2009 RoyaltyShare, Inc.   All Rights Reserved
#------------------------------------------------------------
package RPS::Mechanical::Process::CreateStatement::Complex;

# This will serve as the base for the 'complicated' mechanical statement processes.
# For now, that means US and CA mechanicals... UK runs are much simpler.
use strict;

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

use POSIX qw(ceil);
use Data::Dumper;

use Common::Assert;
use Common::DB::Item::Client;
use Common::Log;
use Common::RSMath;
use Common::Util;
use RPS::Mechanical::Process::Exception;
use RPS::DB::Item::ReserveLiquidation;
use RPS::DB::Item::FinanceAccount;
use RPS::DB::Item::FinanceTransaction;
use RPS::DB::Item::PendingTransaction;
use RPS::DB::Item::MechanicalRunTrackLicense;
use RPS::DB::Item::MechanicalStatementLicenseTransaction;
use RPS::DB::Item::Product;
use RPS::DB::Item::ProductType;
use RPS::License::LicenseType;
use RPS::License::RateType;
use RPS::License::RateBasis;
use RPS::Publisher::Status;

use base 'RPS::Mechanical::Process::CreateStatement';



#
# Virtual methods
#


sub _defaultIssueStatRateID
{
    assert(0, 'override this');
}


sub _currencyCode
{
    my ($self) = @_;
    assert(0, 'override this');
}

sub _getStatRates
{
    my ($self) = @_;
    assert(0, 'override this');
}

sub _createLicenseReserve
{
    my ($self, $statementItem, $units, $periodsRemaining);
    assert(0, 'override this');
}


sub _getLicensesWithPendingTransactions
{
    my ($self) = @_;
    assert(0, 'override this');
}

sub _ringtoneStatRateType
{
    my ($self) = @_;
    assert(0, 'override this');
}

sub _reserveType
{
    my ($self) = @_;
    assert(0, 'override this');
}

sub _lookupTrackLicense
{
    my ($self, $trackLicenseID) = @_;
    assert(0, 'override this');
}

sub _createStatementItem
{
    my ($self, $statementID, $trackLicenseID, $productID, $upc, $isrc, $saleStatRateID, $issueStatRateID) = @_;
    assert(0, 'override this');
}

sub _createStatementLicense
{
    my ($self, $trackLicense) = @_;
    assert(0, 'override this');
}

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


sub _getStatementTrackItemsByStatementID
{
    my ($self, $statementID) = @_;
    assert(0, 'override this');
}

sub _getFileLabel
{
    my ($self, $run) = @_;
    assert(0, 'override this');
}

sub _lookupPublisher
{
    my ($self, $publisherID) = @_;
    assert(0, 'override this');
}

sub _lookupPublisherAccount
{
    my ($self, $publisherID, $payorID) = @_;
    assert(0, 'override this');
}

sub _lookupStatementPublisherAccount
{
    my ($self, $publisherID, $payorID, $indirectPublisherID) = @_;
    assert(0, 'override this');
}

sub _createStatementPublisherAccount
{
    my ($self, $publisherID, $payorID, $indirectPublisherID) = @_;
    assert(0, 'override this');
}

sub _createPublisherAccount
{
    my ($self, $publisherID, $payorID) = @_;
    assert(0, 'override this');
}

sub _getIndirectPublisherIDs
{
    my ($self, $statementID) = @_;
    assert(0, 'override this');
}

sub _getPublisherStatement
{
    my ($self, $statementID, $publisherID) = @_;
    assert(0, 'override this');
}

sub _getStatementTransactionItemsByStatementID
{
    my ($self, $statementID) = @_;
    assert(0, 'override this');
}

sub _getStatementPublisherTransactionItems
{
    my ($self, $statementID, $publisherID) = @_;
    assert(0, 'override this');
}

sub _publisherIDFromStatementTrackItem
{
    my ($self, $trackItem) = @_;
    assert(0, 'override this');
}

sub _publisherIDFromTrackLicense
{
    my ($self, $trackLicense) = @_;
    assert(0, 'override this');
}

sub _publisherIDFromStatementItem
{
    my ($self, $statement) = @_;
    assert(0, 'override this');
}

sub _idFromTrackLicense
{
    my ($self, $trackLicense) = @_;
    assert(0, 'override this');
}

sub _idFromStatementItem
{
    my ($self, $statementItem) = @_;
    assert(0, 'override this');
}

sub _idFromStatement
{
    my ($self, $statement) = @_;
    assert(0, 'override this');
}

sub _publisherIDFromStatement
{
    my ($self, $statement) = @_;
    assert(0, 'override this');
}

sub _idFromStatementLicense
{
    my ($self, $statementLicense) = @_;
    assert(0, 'override this');
}

sub _lookupPublisherTrackCrossedLicenseAccount
{
    my ($self, $publisherID, $trackID, $payorID) = @_;
    assert(0, 'override this');
}

sub _createPublisherTrackCrossedLicenseAccount
{
    my ($self, $publisherID, $trackID, $payorID, $accountID) = @_;
    assert(0, 'override this');
}

sub _getStatementPublisherFromAccount
{
    my ($self, $account) = @_;
    assert(0, 'override this');
}

sub _getIndirectAccountByPayorIndirectPublisherID
{
    my ($self, $payorID, $publisherID) = @_;
    assert(0, 'override this');   
}

sub _getStatementCrossedLicenseTransactions
{
    my ($self, $statementID, $publisherID, $trackID) = @_;
    assert(0, 'override this');
}

sub _idFromStatementLicenseItem
{
    my ($self, $licenseItem) = @_;
    assert(0, 'override this');
}

sub _trackLicenseIDFromStatementLicenseItem
{
    my ($self, $licenseItem) = @_;
    assert(0, 'override this');
}

sub _getStatementLicenseTransactionsByStatementLicenseID
{
    my ($self, $statementLicenseID) = @_;
    assert(0, 'override this');
}

sub _createStatementLicenseTransaction
{
    my ($self, $licenseItemID) = @_;
    assert(0, 'override this');
}

sub _getAllCommittedLicenseReservesByTrackLicenseID
{
    my ($self, $trackLicenseID) = @_;
    assert(0, 'override this');
}

sub _createLicenseReserveRunItem
{
    my ($self, $reserve) = @_;
    assert(0, 'override this');
}

sub _getAllMechanicalCarryoverByTrackLicenseID
{
    my ($self, $trackLicenseID) = @_;
    assert(0, 'override this');
}

sub _createMechanicalRunCarryover
{
    my ($self, $carryover) = @_;
    assert(0, 'override this');
}

sub _getLicensesForPublisherStatementWithTrackAndRegion
{
    my ($self, $trackID, $regionID, $startDate, $endDate, $publisherID) = @_;
    assert(0, 'override this');
}

sub _getPaidSaleRunMapForLicense
{
    my ($self, $license) = @_;
    assert(0, 'override this');
}

sub _lookupMechanicalStatementItem
{
    my ($self, $statementItemID) = @_;
    assert(0, 'override this');
}

sub _getLastStatementAdjustmentItem
{
    my ($self, $statementItemID) = @_;
    assert(0, 'override this');
}

sub _idFromStatementAdjustmentItem
{
    my ($self, $lastAdjustment) = @_;
    assert(0, 'override this');
}

sub _idFromRunItem
{
    my ($self, $run) = @_;
    assert(0, 'override this');
}

sub _createStatementAdjustmentItem
{
    my ($self, $ccompLicense, $originalStatementItemID, $lastAdjustmentID, $adjustmentAmount, $total) = @_;
    assert(0, 'override this');
}

sub _createStatementPublisher
{
    my ($self, $publisherID) = @_;
    assert(0, 'override this');
}

sub _publisherIDFromStatementPublisher
{
    my ($self, $publisherID) = @_;
    assert(0, 'override this');
}

sub _createStatementTrack
{
    my ($self, $publisherID, $trackID, $albumID, $songTitle) = @_;
    assert(0, 'override this');
}

sub _createStatementCrossedLicenseTransaction
{
    my ($self, $statementLicense, $publisherID, $trackID, $pendingTrans) = @_;
    assert(0, 'override this');
}

sub _createStatementTransactionFromPendingTransaction
{
    my ($self, $pendingTransaction) = @_;
    assert(0, 'override this');
}

sub _getLicensesByTrackIDAndDate
{
    my ($self, $trackID, $startDate, $endDate) = @_;
    assert(0, 'override this');
}

sub _getLicensesByTrackID
{
    my ($self, $trackID) = @_;
    assert(0, 'override this');
}


# !!! FROM BASE CLASS - OVERRIDE THESE :
# !!! sub _testAlreadyPaidSaleWithLicense
#
# !!! sub _getStatementDBItem
#
# !!! AND THIS: sub _runType {     my ($self) = @_;     return RPS::DB::Item::SaleRunMap::kRunTypeMechanical; }
# !!! sub _getRunDBItem
#
# !!! sub _getSales



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

    $self->_report("_init args:", 4);
    $self->_report(\%args, 4);


    $self->SUPER::_init(%args);

    my $publisherID = $self->_publisherID();
    
    # We'll increment these variables as we go...
    #
    $self->{_physicalSubtotal} = 0;
    $self->{_digitalSubtotal} = 0;

    return $self;
}


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

    my $statementID = $self->_statementID();
    Common::Log::Print("COMMITTING STATEMENT $statementID\n");


    # Commit advances at the license level.
    #
    my $licenseItems = $self->_getStatementLicenseItemsByStatementID($statementID);
    while ($licenseItems->hasNext())
    {
        my $licenseItem = $licenseItems->next();

        $self->_applyLicenseAdvance($licenseItem);
    }


    # Need to apply pending transactions and such to the crossed license accounts for each track.
    #
    my $allTrackItems = $self->_getStatementTrackItemsByStatementID($statementID);
    while (my $trackItem = $allTrackItems->next())
    {
        $self->_applyCrossedLicenseAdvance($trackItem);
    }

    # Commit advances at the publisher level.
    #
    $self->_applyPublisherAdvance();
    
    # Now commit advances at the statement publisher level.
    #
    $self->_applyStatementPublisherAdvance();    


    # !!! Not going to even implement the handleCarryover logic, since
    # we don't do that anymore.
    #
}


sub _fileLabel
{
    my ($self) = @_;
    if (! $self->{_fileLabel})
    {
        my $run = $self->_getRunDBItem();
        $self->{_fileLabel} = $self->_getFileLabel($run);
    }

    return $self->{_fileLabel};
}


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


    my $statement = $self->_getStatementDBItem();
    my $statementID = $self->_idFromStatement($statement);


    my $publisherID = $self->_publisherIDFromStatement($statement);
    my $payorID = $statement->payor_id;

    # Create a new publisher_id/payor_id/account_id mapping if necessary
    #
    my $accountMap = $self->_lookupPublisherAccount($publisherID, $payorID);
    if (! $accountMap)
    {
        $accountMap = $self->_createPublisherAccount($publisherID, $payorID);
        $accountMap->save();
    }


    # Create a new finance account if necessary
    #
    my $accountID = $accountMap->finance_account_id;
    if (! $accountID)
    {
        # Create a new account 
        #
        my $account = RPS::DB::Item::FinanceAccount->Create
        (
            # !!! Might be nice to note the run type in the description...
            #
            description => "holdover account for publisher $publisherID payor $payorID",
            type_code => RPS::DB::Item::FinanceAccount::kAccountTypeHoldover,
            currency_code => $self->_currencyCode(),
        );
        $account->save();
        $accountID = $account->finance_account_id;


        # save the account_id in the map table
        #
        $accountMap->finance_account_id($accountID);
        $accountMap->save();
    }

    # Apply the statement subtotal as a transaction to this account.
    #
    # !!! We want the memo to be the run name - I'd like a consistent interface around that.
    my $statementSubtotal = $statement->subtotal;
    if ($statementSubtotal < 0 or $statementSubtotal > 0)
    {
        my $transaction = RPS::DB::Item::FinanceTransaction->Create
        (
            finance_account_id => $accountID,
            amount => Common::RSMath::round($statementSubtotal, 2),
            transaction_date => Common::DB::Item::kDateTimeNow(),
            type_code => RPS::DB::Item::FinanceTransaction::kTypeClosingBalance,
            memo => "mechanical statement $statementID royalties owed",
            currency_code => $self->_currencyCode(),
        );
        $transaction->save();
    }

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


        # !!! Note that amount == amount. no sign flipping.  We have to get the sign right when we create the
        # !!! pending transaction in the first place.
        #
        my $transaction = RPS::DB::Item::FinanceTransaction->Create
        (
            finance_account_id => $pending->finance_account_id,
            amount => Common::RSMath::round($pending->amount, 2),
            type_code => $pending->type_code,
            transaction_date => $pending->transaction_date,
            memo => $pending->memo,
            check_number => $pending->check_number,
            currency_code => $pending->currency_code,
        );
        $transaction->save();

        # Delete the pending transaction once we're done with it.
        #
        $pending->delete();
    }



}


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

    Common::Log::Print("_applyStatementPublishingAdvance");

    my $statement = $self->_getStatementDBItem();
    my $statementID = $self->_idFromStatement($statement);
    my $payorID = $statement->payor_id;
    my $publisherID = $self->_publisherIDFromStatement($statement);
    my $publisher = $self->_lookupPublisher($publisherID);
    
    my @subPublisherIDs = $self->_getSubPublisherIDs($statementID);
    Common::Log::Print("sub publisher ids:", \@subPublisherIDs);
 
    foreach my $subPublisherID (@subPublisherIDs)  
    {
        # The statement's publisher isn't also a 'sub publisher', so skip it.
        #
        next if ($subPublisherID == $publisherID);

        if (defined $subPublisherID)
        {
            my $publisherStatement = $self->_getPublisherStatement($statementID, $subPublisherID);
            Common::Log::Print("publisher statement", $publisherStatement);
            
            # Create a new publisher_id/payor_id/account_id mapping if necessary
            #
            my $accountMap = $self->_lookupStatementPublisherAccount($subPublisherID, $payorID, $publisherID);
            if (! $accountMap)
            {
                $accountMap = $self->_createStatementPublisherAccount($subPublisherID, $payorID, $publisherID);
                $accountMap->save();
            }
        
        
            # Create a new finance account if necessary
            #
            my $accountID = $accountMap->finance_account_id;
            if (! $accountID)
            {
                # Create a new account 
                #
                my $account = RPS::DB::Item::FinanceAccount->Create
                (
                    # !!! Might be nice to note the run type in the description...
                    #
                    description => "holdover account for publisher $subPublisherID payor $payorID indirect publisher $publisherID",
                    type_code => RPS::DB::Item::FinanceAccount::kAccountTypeHoldover,
                    currency_code => $self->_currencyCode(),
                );
                $account->save();
                $accountID = $account->finance_account_id;
        
        
                # save the account_id in the map table
                #
                $accountMap->finance_account_id($accountID);
                $accountMap->save();
            }
        
            # Now apply all pending transactions, and delete them.
            #
            my $transactionTotal=0;

            my $transactionItems = $self->_getStatementPublisherTransactionItems($statementID, $subPublisherID);
            while (my $item = $transactionItems->next())
            {
                # Fetch the 'real' pending transaction.
                #
                my $pending = RPS::DB::Item::PendingTransaction->Lookup(pending_transaction_id => $item->pending_transaction_id);
        
                $transactionTotal += $pending->amount;
        
                # !!! Note that amount == amount. no sign flipping.  We have to get the sign right when we create the
                # !!! pending transaction in the first place.
                #
                my $transaction = RPS::DB::Item::FinanceTransaction->Create
                (
                    finance_account_id => $pending->finance_account_id,
                    amount => Common::RSMath::round($pending->amount, 2),
                    type_code => $pending->type_code,
                    transaction_date => $pending->transaction_date,
                    memo => $pending->memo,
                    check_number => $pending->check_number,
                    currency_code => $pending->currency_code,
                );
                $transaction->save();
        
                # Delete the pending transaction once we're done with it.
                #
                $pending->delete();
            }


            # We'll need to know what the current account balance is.
            # If the amount due on this statement > 0, then we will clear out
            # the balance on the account by applying a new transaction.
            #
            # If the amount due is 0, and the statement_total < 0, then we need to apply
            # the royalties owed as a transaction into this account.
            #
            # One more scenario:  If this publisher is 'on hold', then we want to carry 
            # the balance forward even if it is positive (and not have it roll up to the 
            # admin's balance).
            #
            # !!! And another thing...
            #     If there is a transaction (negative, at least), but the publisher balance is positive, then
            #     we need to make sure that we 'cancel out' the negative amount.  Otherwise it ends up appearing
            #     as a previous balance on the next statement, which is wrong.
            #
            my $newAccountBalance = $publisherStatement->previous_balance() + $transactionTotal;


            my $statementEndingBalance = $publisherStatement->statement_total;
            my $statementSubtotal = $publisherStatement->subtotal;
            Common::Log::Print("account id: $accountID,  statementEndingBalance: $statementEndingBalance, statementSubtotal $statementSubtotal");
            if (($publisherStatement->on_hold() || $statementEndingBalance < 0) && $statementSubtotal > 0)
            {
                my $transaction = RPS::DB::Item::FinanceTransaction->Create
                (
                    finance_account_id => $accountID,
                    amount => Common::RSMath::round($statementSubtotal, 2),
                    transaction_date => Common::DB::Item::kDateTimeNow(),
                    type_code => RPS::DB::Item::FinanceTransaction::kTypeClosingBalance,
                    memo => "mechanical statement $statementID royalties owed",
                    currency_code => $self->_currencyCode(),
                );
                $transaction->save();
            }
#            elsif ($newAccountBalance != 0)
#            elsif (! $publisherStatement->on_hold() && $newAccountBalance > 0)
            # !!! Need the != 0 to account for the situation where there is a positive total amount due (so it rolls up),
            # !!! but there is a negative net transaction total.  Otherwise that negative transaction becomes a bogus
            # !!! forward balance.
            # !!! JPK - But I am not actually testing to see that there is a positive total amount due here.
            # !!! So we're creating some bogus transactions...
#            elsif (! $publisherStatement->on_hold() && $newAccountBalance != 0)
            elsif (! $publisherStatement->on_hold() && $newAccountBalance != 0 && $publisherStatement->amount_due() > 0)
            {
                my $transaction = RPS::DB::Item::FinanceTransaction->Create
                (
                    finance_account_id => $accountID,
                    amount => Common::RSMath::round((-1 * $newAccountBalance), 2),
                    transaction_date => Common::DB::Item::kDateTimeNow(),
                    type_code => RPS::DB::Item::FinanceTransaction::kTypeClosingBalance,
                    memo => "See Admin",
                    currency_code => $self->_currencyCode(),
                );
                $transaction->save();
            }
    
        }
    }
        
}


sub _applyCrossedLicenseAdvance
{
    my ($self, $trackItem) = @_;

    my $statement = $self->_getStatementDBItem();

    # Is there any activity?
    #
    if (0 != $trackItem->crossed_previous_advance_balance
     || 0 != $trackItem->crossed_transaction_subtotal
     || 0 != $trackItem->crossed_advance_balance)
    {
        # See if we need to create a new account and mapping.
        #
        my $publisherID = $self->_publisherIDFromStatementTrackItem($trackItem);
        my $trackID = $trackItem->track_id;
        my $payorID = $statement->payor_id;

        my $statementID = $self->_idFromStatement($statement);

        my $accountID;
        my $crossedAccountMap = $self->_lookupPublisherTrackCrossedLicenseAccount($publisherID, $trackID, $payorID);

        if ($crossedAccountMap)
        {
            $accountID = $crossedAccountMap->finance_account_id;
        }
        
        if (! $accountID)
        {
            my $account = RPS::DB::Item::FinanceAccount->Create
            (
                description => "account for crossed licenses, publisher $publisherID, track $trackID, payor $payorID",
                type_code => RPS::DB::Item::FinanceAccount::kAccountTypeAdvance,
                currency_code => $self->_currencyCode(),
            );
            $account->save();

            $accountID = $account->finance_account_id;

            # Create the map if necessary.
            #
            if (! $crossedAccountMap)
            {
                $crossedAccountMap = $self->_createPublisherTrackCrossedLicenseAccount($publisherID, $trackID, $payorID, $accountID);
            }
            $crossedAccountMap->save();
        }


        # Now apply all pending transactions, and delete them.
        #
        my $transactionItems = $self->_getStatementCrossedLicenseTransactions($statementID, $publisherID, $trackID);


        while (my $item = $transactionItems->next())
        {
            # Fetch the 'real' pending transaction.
            #
            my $pending = RPS::DB::Item::PendingTransaction->Lookup(pending_transaction_id => $item->pending_transaction_id);


            # !!! Note that amount == amount. no sign flipping.  We have to get the sign right when we create the
            # !!! pending transaction in the first place.
            #
            my $transaction = RPS::DB::Item::FinanceTransaction->Create
            (
                finance_account_id => $pending->finance_account_id,
                amount => Common::RSMath::round($pending->amount, 2),
                transaction_date => $pending->transaction_date,
                type_code => $pending->type_code,
                memo => $pending->memo,
                check_number => $pending->check_number,
                currency_code => $pending->currency_code,
            );
            $transaction->save();


            # Done with the pending transaction, get rid of it.
            #
            $pending->delete();
        }


        # Now, figure out what sort of adjustment to make on this account based on what happened
        # on this statement.
        #
        my $amount = $trackItem->crossed_advance_balance - ($trackItem->crossed_previous_advance_balance + $trackItem->crossed_transaction_subtotal);
        
        if (0 != $amount)
        {
            my $transaction = RPS::DB::Item::FinanceTransaction->Create
            (
                finance_account_id => $accountID,
                amount => Common::RSMath::round($amount, 2),
                transaction_date => Common::DB::Item::kDateTimeNow(),
                type_code => RPS::DB::Item::FinanceTransaction::kTypeClosingBalance,
                memo => "mechanical run ".$self->_fileLabel() .", statement ".$self->_statementID(),
                currency_code => $self->_currencyCode(),
            );
            $transaction->save();

            $self->_report("   applied transaction amount $amount from license advance account $accountID",2);
        }
    }
}


sub _applyLicenseAdvance
{
    my ($self, $licenseItem) = @_;

    my $statementLicenseID = $self->_idFromStatementLicenseItem($licenseItem);

    my $transactions = $self->_getStatementLicenseTransactionsByStatementLicenseID(
        $statementLicenseID
    );

    # Is there any activity?
    #
    if (0 != $licenseItem->previous_advance_balance
     || 0 != $transactions->size()
     || 0 != $licenseItem->advance_balance)
    {

        # Fetch the license, and see if we need to create the advance account.
        #
        my $licenseID = $self->_trackLicenseIDFromStatementLicenseItem($licenseItem);

        my $trackLicense = $self->_getTrackLicense($licenseID);

        my $accountID = $trackLicense->finance_account_id;
        if (! $accountID)
        {
            # Creating the advance account.
            # -- Note that the starting balance is theoretically in two places:
            #    Both in the license table, and in the mechanical_statement_table as
            #    the 'previous_balance' column.
            #    I'm going to use the value in the license table...
            #
            my $account = RPS::DB::Item::FinanceAccount->Create
            (
                description => "advance account for track license $licenseID",
                type_code => RPS::DB::Item::FinanceAccount::kAccountTypeAdvance,
                currency_code => $self->_currencyCode(),
            );
            $account->save();

            $accountID = $account->finance_account_id;

            # Update the license with the account id.
            #
            $trackLicense->finance_account_id($accountID);
            $trackLicense->save();
            $self->_report("      created new advance account $accountID for track license $licenseID", 2);
        }


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


            # !!! Note that amount == amount. no sign flipping.  We have to get the sign right when we create the
            # !!! pending transaction in the first place.
            #
            my $transaction = RPS::DB::Item::FinanceTransaction->Create
            (
                finance_account_id => $pending->finance_account_id,
                amount => Common::RSMath::round($pending->amount, 2),
                transaction_date => $pending->transaction_date,
                type_code => $pending->type_code,
                memo => $pending->memo,
                check_number => $pending->check_number,
                currency_code => $pending->currency_code,
            );
            $transaction->save();


            # Done with the pending transaction, get rid of it.
            #
            $pending->delete();
        }


        # Now, figure out what sort of adjustment to make on this account based on what happened
        # on this statement.
        #
        my $amount = $licenseItem->advance_balance - ($licenseItem->previous_advance_balance + $licenseItem->transaction_subtotal);
        
        if (0 != $amount)
        {
            my $transaction = RPS::DB::Item::FinanceTransaction->Create
            (
                finance_account_id => $accountID,
                amount => Common::RSMath::round($amount, 2),
                transaction_date => Common::DB::Item::kDateTimeNow(),
                type_code => RPS::DB::Item::FinanceTransaction::kTypeClosingBalance,
                memo => "mechanical run ".$self->_fileLabel() .", statement ".$self->_statementID(),
                currency_code => $self->_currencyCode(),
            );
            $transaction->save();

            $self->_report("   applied transaction amount $amount from license advance account $accountID",2);
        }
    }
}


# This method will receive all 'possible' licenses, and then deal with reserves and carryover
#
sub _processLicense
{
    my ($self, $license) = @_;

    $self->_report("_processLicense", 4);
    $self->_report($license, 4);

    if (! $self->_licenseIsValid($license))
    {
        next;
    }


    # Skip this license if the album it appears on is inactive.
    #
    my $track = $self->_getTrack($license->track_id);
    my $album = $self->_getAlbum($track->album_id);
    if ($album->inactive() || $album->status() == 0)
    {
        return;
    }


    $self->_processLicenseReserve($license);

    $self->_processLicenseCarryover($license);

    $self->_processControlledCompAdjustments($license);
}




sub _finishProcessLicenses
{
    my ($self) = @_;
    $self->_report("_finishProcessLicenses", 3);

    # Determine all licenses that might be associated with this publisher.
    #
    # This is casting too wide a net.
    # At this point we've created 'statement license' table entries for all sale-related activity.
    # The only thing we may have missed would be accounts with pending transactions that otherwise
    # have no activity.
    # But if I just call 'finishProcessLicense' on all _possible_ licenses, I'll get statement_license entries
    # for every license.  Which, ya know, makes sense now that I think about it.
    #
    # So instead I'll need to make a list of licenses that we've looked at, and marry that with any licenses
    # that we might have skipped but do actually have pending transactions.
    #
    my %licenses;

    my $licenseItems = $self->_getStatementLicenseItemsByStatementID($self->_statementID());
    while ($licenseItems->hasNext())
    {
        my $licenseItem = $licenseItems->next();
        my $trackLicenseID = $self->_trackLicenseIDFromStatementLicenseItem($licenseItem);

        my $license = $self->_lookupTrackLicense($trackLicenseID);
        $licenses{$trackLicenseID} = $license;
    }

    my $pendingLicenses = $self->_getLicensesWithPendingTransactions();
    while (my $pendingLicense = $pendingLicenses->next())
    {
        my $trackLicenseID = $self->_idFromTrackLicense($pendingLicense);
        if (! $licenses{$trackLicenseID})
        {
            $licenses{$trackLicenseID} = $pendingLicense;
        }
    }

    foreach my $trackLicenseID (keys %licenses)
    {
        my $license = $licenses{$trackLicenseID};
        $self->_finishProcessLicense($license);
    }

}

sub _finishProcessLicense
{
    my ($self, $license) = @_;

    $self->_report("_finishProcessLicense", 3);
    $self->_report($license, 4);

    my $statementLicense = $self->_getStatementLicense($license);
    my $statementTrack = $self->_getStatementTrack($self->_publisherIDFromTrackLicense($license), $license->track_id, $statementLicense);

    $self->_report('statementLicense', 4);
    $self->_report($statementLicense, 4);


    my $licenseItemID = $self->_idFromStatementLicense($statementLicense);


    # This is the subtotal from all the sales, returns, carryover, etc.
    #
    my $subtotal = $statementLicense->subtotal();


    # Keep track of the stuff we're going to want in the 
    # MechanicalStatementLicense object.
    # ... and the MechanicalStatementPublisher object.
    # If we end up with pending transactions or sales, we'll create one of those.
    #

    # First, let's grab the account info.
    # We might have pending transactions.
    #
    my $accountID = $license->finance_account_id();
    my $pendingTransactions;
    my $total = 0;
    my $previousAdvanceBalance = 0;
    my $previousTransactionSubtotal = 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);
        $self->_report("      advance account current balance = $previousAdvanceBalance", 3);
    }
    else
    {
        $self->_report("      no advance account established . advance amount = $previousAdvanceBalance", 3);
    }


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

            my $licenseTransItem = $self->_createStatementLicenseTransaction($licenseItemID);

	        $licenseTransItem->amount($pendingTrans->amount);
	        $licenseTransItem->memo($pendingTrans->memo);
	        $licenseTransItem->check_number($pendingTrans->check_number);
	        $licenseTransItem->pending_transaction_id($pendingTrans->pending_transaction_id);
	        $licenseTransItem->transaction_date($pendingTrans->transaction_date);
	        $licenseTransItem->type_code($pendingTrans->type_code);
                                
            $licenseTransItem->save();
        }
    }

    

    my $isCrossed = $license->cross_collateralized;

    # subtotal - amount due before account factored in.
    #
    my $adjustedSubtotal = $subtotal + $previousAdvanceBalance + $pendingTransactionSubtotal;

                        
    # Here we diverge for crossed licenses.
    #
    my $appliedToAdvance;
    my $advanceBalance;
    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;

        # Increment the track item's crossed subtotal value.
        # We're going to do this as we go, then caclulate the crossed totals and adjusted
        # subtotals, etc. later. (when we're done looking at licenses)
        #
        $statementTrack->crossed_subtotal( $statementTrack->crossed_subtotal() + $total );

        # Note that we do _not_ apply this to the statement track total yet - We might
        # end up carrying a crossed balance on this track.
        #
    }
    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;
        }

        # Since this is not crossed, we can apply the total to the track
        #
        $statementTrack->subtotal( $statementTrack->subtotal() + $total );
        $appliedToAdvance = $advanceBalance - $previousAdvanceBalance;
    }
                        


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

    $statementLicense->save();
}



sub _processLicenseReserve
{
    my ($self, $trackLicense) = @_;

    $self->_report('_processLicenseReserve', 4);
    my $trackLicenseID = $self->_idFromTrackLicense($trackLicense);


    my $reserves = $self->_getAllCommittedLicenseReservesByTrackLicenseID($trackLicenseID);
    while (my $reserve = $reserves->next())
    {
        $self->_report($reserve, 4);

        my $productID = $reserve->product_id;
        my $issueStatRateID = $self->_defaultIssueStatRateID($trackLicense->type);
        my $saleStatRateID = $reserve->sale_stat_rate_id;


        # We use '0' for the stat rate ids for penny rate licenses.
        # This is because this id is irrelevant, and we want the data to hash
        # to the same bucket.
        #
        # !!! Need another abstract 'type' package.
        #
        if (RPS::License::RateType::kPenny == $trackLicense->rate_type)
        {
            $issueStatRateID = $saleStatRateID = 0;
        }

        my $period = $reserve->periods_remaining;

        if (1 == $period)
        {
            # We're liquidating this reserve.
            #
            my $units = $reserve->units;

            $self->{_productUnitMap}{$trackLicenseID}{$productID}{$issueStatRateID}{$saleStatRateID}{liquidated} += $units;
        }


        # Associate this reserve with this run, so we can increment its periods_remaining when we commit
        #
        my $runMapItem = $self->_createLicenseReserveRunItem($reserve);
        $runMapItem->save();
    }
}


# !!! I am hoping we can GET RID of this bit of code...
#
sub _processLicenseCarryover
{
    my ($self, $trackLicense) = @_;

    my $trackLicenseID = $self->_idFromTrackLicense($trackLicense);

    my $carryovers = $self->_getAllMechanicalCarryoverByTrackLicenseID($trackLicenseID);
    return unless $carryovers;

    while (my $carryover = $carryovers->next())
    {
        my $productID = $carryover->product_id;
        my $issueStatRateID = $self->_defaultIssueStatRateID($trackLicense->type);
        my $saleStatRateID = $carryover->sale_stat_rate_id;
        my $units = $carryover->units;

        # We use '0' for the stat rate ids for penny rate licenses.
        # This is because this id is irrelevant, and we want the data to hash
        # to the same bucket.
        #
        if (RPS::License::RateType::kPenny == $trackLicense->rate_type)
        {
            $issueStatRateID = $saleStatRateID = 0;
        }

        $self->{_productUnitMap}{$trackLicenseID}{$productID}{$issueStatRateID}{$saleStatRateID}{carryover} += $units;


        # We're going to liquidate this carryover this run, so make a note of that.
        #
        my $mechanicalRunCarryoverItem = $self->_createMechanicalRunCarryover($carryover);
        $mechanicalRunCarryoverItem->save();
    }
}



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

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

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

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

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

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

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

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


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


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


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

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

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

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

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


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

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

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

    return \@trackLicenses;
}

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

    my @tracks;
    my $productID = $sale->product_id;
    my $serviceID = $self->_fileIDToService($sale->file_id);

    my $productTracks = RPS::DB::Item::ProductTrack->GetTracksByProductID($productID);
    while (my $pt = $productTracks->next())
    {
        $self->_report("product_track", 4);
        $self->_report($pt, 4);
        if ($pt->is_bonus_track())
        {
            $self->_report(" is bonus track", 4);
            my $bonusTrackEntry = RPS::DB::Item::BonusTrack->Lookup
            (
                track_id => $pt->track_id,
                product_id => $productID,
                service_id => $serviceID,
            );
            if (! $bonusTrackEntry)
            {
                $self->_report("   skipping - bonus track not on this product for this service", 4);
                # This track does not appear on this product for this service.
                #
                next;
            }
        }
        $self->_report("   adding track " . $pt->track_id . " to list", 4);
        push @tracks, $pt->track_id;
    }

    return @tracks;
}


sub _processControlledCompAdjustments
{
    my ($self, $ccompLicense) = @_;

    $self->_report('_processControlledCompAdjustments', 3);
    $self->_report($ccompLicense, 4);

    # Ignore this if it isn't actually a controlled comp license.
    #
    return if (RPS::License::LicenseType::kControlledComposition ne $ccompLicense->type);


    # What album does this license apply to?  We'll need the album_id to figure out the per-track rate.
    #
    my $track = $self->_getTrack($ccompLicense->track_id);
    my $albumID = $track->album_id;
    my $controlledCompositionClause = $self->_getControlledCompFromAlbumID($albumID);
    assert($controlledCompositionClause, "Unable to fetch ccomp clause for album $albumID, associated with license: " . Dumper($ccompLicense));

    my $issueStatRateID = $self->_defaultIssueStatRateID($ccompLicense->type);


    # Find any previous sales we've processed for this license.
    #
    # !!! Going to require some DB voodoo
    #
    my $previousSaleMapItems = $self->_getPaidSaleRunMapForLicense($ccompLicense);
    while (my $previousMap = $previousSaleMapItems->next())
    {
        $self->_report(" previousSaleMapItem: ", 4);
        $self->_report($previousMap, 4);

        # fetch the original statement item, so we can examine how much we paid.
        #
        my $originalStatementItemID = $previousMap->statement_item_id;
        my $originalStatementItem = $self->_lookupMechanicalStatementItem($originalStatementItemID);
        my $previousTotal = $originalStatementItem->amount_paid;

        # Did we already adjust this?
        #
        my $lastAdjustmentID = 0;
        my $lastAdjustment = $self->_getLastStatementAdjustmentItem($originalStatementItemID);
        if ($lastAdjustment)
        {
            $self->_report("   previous adjustment: ", 4);
            $self->_report($lastAdjustment, 4);

            $previousTotal = $lastAdjustment->new_total;
            $lastAdjustmentID = $self->_idFromStatementAdjustmentItem($lastAdjustment);
        }

        # Ok, calculate the rate we get with _today's_ licenses.
        #
        # !!! If I want to allow the sale's product config to affect which licenses are taken into account for the ccomp
        # !!! calculations, I'll need to determine that somehow from the sale_run_map entry.
        #
        my $originalProductID = $originalStatementItem->product_id;
        my $originalProduct = $self->_getProduct($originalProductID);

        my $saleStatRateID = $originalStatementItem->sale_stat_rate_id;
        my $effectivePerTrackRate = $self->_calculateEffectivePerTrackRate($albumID, $issueStatRateID, $saleStatRateID, $controlledCompositionClause, $ccompLicense,$originalProduct->product_type_id);

        # Calculate the 'new' total
        #
        my $share = $ccompLicense->share;
        my $units = $originalStatementItem->net_units;
        my $effectiveRate = ($effectivePerTrackRate * ($share / 100));
        my $total = Common::RSMath::round($units * $effectiveRate, 4);

        my $adjustmentAmount = $total - $previousTotal;
        $self->_report("  share = $share, units = $units, perTrackRate = $effectivePerTrackRate", 3);
        $self->_report("  effectiveRate = $effectiveRate.  old total = $previousTotal.  new total: $total", 3);

        if ($adjustmentAmount < 0.01)
        {
            $self->_report("  adjustmentAmount = $adjustmentAmount, not creating a new adjustment item", 3);
            next;
        }


        # OK!  We can now create an adjustment item
        # We're going to need to remember the amount so we can apply this to the license subtotal.
        # !!! Wait, can't we do that right here?
        #
        my $adjustmentItem = $self->_createStatementAdjustmentItem($ccompLicense, $originalStatementItemID, $lastAdjustmentID, $adjustmentAmount, $total);
        $adjustmentItem->save();
        $self->_report("Created new adjustment item: ", 4);
        $self->_report($adjustmentItem, 4);

        my $statementLicense = $self->_getStatementLicense($ccompLicense);
        $self->_report('previous subtotal' . $statementLicense->subtotal() . ", adding in adjustmentAmount $adjustmentAmount", 4);

        $statementLicense->subtotal($statementLicense->subtotal() + $adjustmentAmount);
    }
}

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

	$self->_report("---> _logSaleNoLicense", 4);

    foreach my $trackID ($self->_getTracksForSale($sale))
    {
        $self->_createRetentionLogEntry($sale, $trackID, 0, 0);
    }
}


sub _logProcessedSaleWithLicense
{
    my ($self, $sale, $trackLicense) = @_;
	$self->_report("---> _logProcessedSaleWithLicense", 4);

    $self->_createRetentionLogEntry($sale, $trackLicense->track_id, $self->_idFromTrackLicense($trackLicense), $trackLicense->share);
}



sub _createRetentionLogEntry
{
    my ($self, $sale, $trackID, $trackLicenseID, $share) = @_;

    my $trackLicense = $self->_lookupTrackLicense($trackLicenseID);
    my $trackLicenseType = 0;
    
    if ($trackLicense)
    {
        $trackLicenseType = $trackLicense->type; 
    }
# JPK - Restore this line when we're ready for RINGBACK support
#    elsif ($sale->format_type eq 'R' || $sale->format_type eq '1')
    elsif ($sale->format_type eq 'R')
    {
        $trackLicenseType = RPS::DB::Item::TrackLicense::kRingtone;
    }
    else
    {
        # If we don't have a track license, and it's not a ringtone, then don't bother logging anything here.
        #
        $self->_report(" no license, not a ringtone... not logging", 4);
        return;
    }

    my $runID = $self->_runID();
    my $runType = $self->_runType();
    my $productID = $sale->product_id;
    
    my $saleStatRateID;
    if ($trackLicenseType == RPS::DB::Item::TrackLicense::kRingtone)
    {
        $saleStatRateID = $self->_getRingStatRateID($sale->date_end);
    }
    else    
    {
        $saleStatRateID = $self->_getStatRateID($sale->date_end);
    }        
    
    my $duration = $self->_getTrackDuration($trackID);

    my $units = $sale->units;
    my $numSales = $sale->sales;
    my $numReturns = $sale->returns;
    my $countryCode = $sale->country_code;

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

    if ($self->_productIsDigital($product))
    {
        if ($units < 0)
        {
            $numReturns = (-1 * $units);
        }
        else
        {
            $numSales = $units;
        }
    }
    else
    {
        $units = $numSales - $numReturns;
    }


    # Calculate what the rate would be, assuming the worst.
    #
    my ($rate)= $self->_calculateEffectiveRate
    (
        $self->_defaultIssueStatRateID($trackLicenseType), 
		$saleStatRateID, 
        RPS::License::RateBasis::kSale, 
        RPS::License::RateType::kFull, 
        0, 
        100, 
        $duration, 
        100,
        0,
        $trackLicense
    );
				

    # !!! Let's add country_code to the mechanical_run_track_license table, and record
    # !!! the country the sale was made in.  Then we can choose which data we care about when
    # !!! creating the retention report.

    my $year = $self->_getYear($sale);

    # Make the update an atomic process.
	$self->_report("_createRetentionLogEntry: tl:$trackLicenseID p:$productID t:$trackID r:$rate u:$units s:$share", 4);
    RPS::DB::Item::MechanicalRunTrackLicense->updateUnits (
            run_id => $runID,
            run_type => $runType,
            track_license_id => $trackLicenseID,
            product_id => $productID,
            track_id => $trackID,
            base_rate => $rate,
            units => $units,
            share => $share,
            country_code => $countryCode,
            year => $year,
    );
    
#    my $logEntry = RPS::DB::Item::MechanicalRunTrackLicense->Lookup
#    (
#        run_id => $runID,
#        run_type => $runType,
#        track_license_id => $trackLicenseID,
#        product_id => $productID,
#        track_id => $trackID,
#        base_rate => $rate
#    );
#
#    if (! $logEntry)
#    {
#        $self->_report("CREATING NEW mechanical_run_track_license:", 4);
#        $logEntry = RPS::DB::Item::MechanicalRunTrackLicense->Create
#        (
#            run_id => $runID,
#            run_type => $runType,
#            track_license_id => $trackLicenseID,
#            product_id => $productID,
#            track_id => $trackID,
#            base_rate => $rate,
#            units => 0,
#            share => $share,
#        );
#        $logEntry->save();
#    }
#
#    $logEntry->units( $logEntry->units() + $units );
#    $logEntry->save();

}



sub _processAlreadyPaidSaleWithLicense
{   
    my ($self, $sale, $trackLicense) = @_;
}


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

    $self->_report('  license: ', 4);
    $self->_report($trackLicense, 4);

    # If the license is public domain, we don't really want to process it.
    #
    if ($trackLicense->type == RPS::DB::Item::TrackLicense::kPublicDomain)
    {
        $self->_report("_processSaleWithLicense : This is public domain, returning", 4);
        return;
    }

    my $productID = $sale->product_id;
    my $product = $self->_getProduct($productID);
    my $releaseDate = $product->release_date;
    my $issueStatRateID = $self->_defaultIssueStatRateID($trackLicense->type);
    
    my $saleStatRateID;
    
    if ($trackLicense->type == RPS::DB::Item::TrackLicense::kRingtone)
    {
        $saleStatRateID = $self->_getRingStatRateID($sale->date_end);
    }
    else    
    {
        $saleStatRateID = $self->_getStatRateID($sale->date_end);
    }


    $self->_report('  product: ',4);
    $self->_report($product,4);


    my $units = $sale->units;
    my $numSales = $sale->sales;
    my $numReturns = $sale->returns;

    if ($self->_productIsDigital($product))
    {
        if ($units < 0)
        {
            $numReturns = (-1 * $units);
        }
        else
        {
            $numSales = $units;
        }
    }
    else
    {
        $units = $numSales - $numReturns;
    }


    # Jot down the share for this product/track/license_id grouping.
    # We'll use this at the end to build the 'undershare' report.
    #
    # !!! BUT only for US sales?  Odd...
    # !!! REVISIT THIS !!!
    #
#    if ($sale->country_code eq 'US')
#    {
#        $self->{_shareMap}{$sale->product_id}{$trackLicense->track_id}{$trackLicense->track_license_id}{units} += $units;
#        $self->{_shareMap}{$sale->product_id}{$trackLicense->track_id}{$trackLicense->track_license_id}{share} = $trackLicense->share;
#    }

        
    # If the license is a penny rate, then we don't want to sort sales by the stat rate id.
    # We want all sales to settle into the same bucket.
    #
    my $tempIssueStatRateID = $issueStatRateID;
    my $tempSaleStatRateID = $saleStatRateID;
    if (RPS::License::RateType::kPenny == $trackLicense->rate_type)
    {
        $tempIssueStatRateID = $tempSaleStatRateID = 0;
    }

    # !!! Previous sales?
    # !!! The current structure has us just SKIPPING them, which is totally incorrect.
    # !!! We will need to refactor both the base CreateStatement class, and the UK::Process::CreateStatement code.


    # So, we're going to save info in a data structure to look at later.
    #
    my $trackLicenseID = $self->_idFromTrackLicense($trackLicense);
    $self->{_productUnitMap}{$trackLicenseID}{$productID}{$tempIssueStatRateID}{$tempSaleStatRateID}{sales} += $numSales;
    $self->{_productUnitMap}{$trackLicenseID}{$productID}{$tempIssueStatRateID}{$tempSaleStatRateID}{returns} += $numReturns;
    push @{$self->{_productUnitMap}{$trackLicenseID}{$productID}{$tempIssueStatRateID}{$tempSaleStatRateID}{saleIDs}}, $sale->sale_id;

}


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

    if (! $self->{_statementPublisher}{$publisherID})
    {
        my $item = $self->_createStatementPublisher($publisherID);
        $self->{_statementPublisher}{$publisherID} = $item;
    }

    return $self->{_statementPublisher}{$publisherID};
}


sub _getStatementTrack
{
    my ($self, $publisherID, $trackID, $statementLicense) = @_;

    if (! $self->{_statementTrack}{$publisherID}{$trackID})
    {
        # !!! Really, why is album_id a key here?  That's silly.
        #
        my $track = $self->_getTrack($trackID);
        my $song = $self->_getSongByTrackID($trackID);
        my $item = $self->_createStatementTrack($publisherID, $trackID, $track->album_id, $song->title);
        $self->{_statementTrack}{$publisherID}{$trackID} = $item;


        # - Get the previous balance, sum up transaction subtotals, etc.
        #
        my $payorID = $self->_payorID();
        my $crossedAccountMap = $self->_lookupPublisherTrackCrossedLicenseAccount($publisherID, $trackID, $payorID);

        if ($crossedAccountMap)
        {
            my $accountID = $crossedAccountMap->finance_account_id;

            if ($accountID)
            {
                $item->crossed_previous_advance_balance( RPS::DB::Item::FinanceAccount->CurrentBalance($accountID));

                my $pendingTransactions = RPS::DB::Item::PendingTransaction->GetAccountTransactions(finance_account_id => $accountID);
                if ($pendingTransactions)
                {
                    my $crossedTransactionSubtotal = 0;
                    while (my $pendingTrans = $pendingTransactions->next())
                    {
                        $crossedTransactionSubtotal += $pendingTrans->amount;

                        my $licenseTransItem = $self->_createStatementCrossedLicenseTransaction($statementLicense, $publisherID, $trackID, $pendingTrans);
                        $licenseTransItem->save();
                    }

                    $item->crossed_transaction_subtotal($crossedTransactionSubtotal);
                }
            }
        }

        # We might need the autoincrement ID, so save it.
        #
        $item->save();

    }

    return $self->{_statementTrack}{$publisherID}{$trackID};
}


sub _getStatementLicense
{
    my ($self, $trackLicense) = @_;

    my $trackLicenseID = $self->_idFromTrackLicense($trackLicense);

    if (! $self->{_statementLicense}{$trackLicenseID})
    {
        my $item = $self->_createStatementLicense($trackLicense);
        $item->save();
        $self->{_statementLicense}{$trackLicenseID} = $item;
    }

    return $self->{_statementLicense}{$trackLicenseID};
}


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

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

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

    return $self->_getSong($master->song_id);
}




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

    if (! $self->{_runID})
    {
        my $run = $self->_getRunDBItem();
        $self->{_runID} = $self->_idFromRunItem($run);
    }
    return $self->{_runID};
}



sub _publisherID
{
    my ($self) = @_;
    if (! $self->{_publisherID})
    {
        my $statement = $self->_getStatementDBItem();
        $self->{_publisherID} = $self->_publisherIDFromStatementItem($statement);
    }
        
    return $self->{_publisherID};
}


#sub _getSales
#{
#    my ($self) = @_;
#
#    # Read the sales to process from the SalePublisherMap table
#    # !!! Actually, I am going to put this interface into Raptor::Sale, so it makes more sense.
#    #
#    my $statement = $self->_getStatementDBItem();
#
#    return Raptor::DB::Item::Sale->GetPublisherMappedSales($statement->publisher_id, $statement->mechanical_run_id, $self->_runType());
#     
#}


sub _finalize
{
    my ($self) = @_;
    $self->_report("_finalize", 4);

    $self->_processProductUnitMap($self->{_productUnitMap});

    $self->_finishProcessLicenses();


    # So at this point, we have:
    # - all the MechanicalStatementItems we'll ever have.
    # - all the MechanicalStatementLicense records
    # - all the MechanicalStatementTrack records
    #
    # Now we need to iterate over the StatementTrack items, in order to
    # calculate their adjusted subtotals, applied to advance, and total values.
    #
    # We will then create the MechanicalStatementPublisher records.
    #
    # Last, we get all the publisher-level account stuff, and transactions, and
    # deal with that.
    #
    # Then fill in the statement totals.

    my $publisherSubtotal = 0;

    foreach my $publisherID (keys %{$self->{_statementTrack}})
    {
        $self->_report("_finalize : calculating statement publisher id $publisherID", 4);
        my $statementPublisher = $self->_getStatementPublisher($publisherID);

        foreach my $trackID (keys %{$self->{_statementTrack}{$publisherID}})
        {
            my $statementTrack = $self->{_statementTrack}{$publisherID}{$trackID};

            $statementTrack->crossed_adjusted_subtotal( 
             $statementTrack->crossed_subtotal() + $statementTrack->crossed_previous_advance_balance() + $statementTrack->crossed_transaction_subtotal() );

            my $crossedTotal;
            if ($statementTrack->crossed_adjusted_subtotal > 0)
            {
                $statementTrack->crossed_advance_balance(0);
                $crossedTotal = $statementTrack->crossed_adjusted_subtotal();
            }
            else
            {
                $statementTrack->crossed_advance_balance( $statementTrack->crossed_adjusted_subtotal() );
                $crossedTotal = 0;
            }
            $statementTrack->crossed_applied_to_advance( $statementTrack->crossed_advance_balance() - $statementTrack->crossed_previous_advance_balance() );
            

            # Now apply the crossed total to the track total
            #
            $statementTrack->subtotal( $statementTrack->subtotal() + $crossedTotal );
            $statementTrack->save();

            $self->_report("_finalize : calculating statement publisher id $publisherID : track $trackID  subtotal now " . $statementTrack->subtotal(), 4);

            my $subtotal = $statementPublisher->subtotal() + $statementTrack->subtotal();
            $statementPublisher->subtotal( $subtotal );
            
            # For now, we'll assume that the amount due and statement total are the same as the subtotal.
            # The transactions may change this, in which case we will update them again later.
            #
            $statementPublisher->amount_due( $subtotal );
            $statementPublisher->statement_total( $subtotal );

            $self->_report("_finalize : calculating statement publisher id $publisherID : track $trackID : publisher subtotal=" . $statementPublisher->subtotal . "  amount_due=" . $statementPublisher->amount_due . " statement_total=" . $statementPublisher->statement_total(), 4);
        }


        my $onHold = 0;
        my $publisherData = $self->_lookupPublisher($publisherID);
        if (RPS::Publisher::Status::kOnHold == $publisherData->status())
        {
            $self->_report("setting the on_hold flag", 4);
            $statementPublisher->on_hold(1);
# !!!            $statementPublisher->amount_due(0);
            # !!! No, we don't want to screw with the amount_due.   We deal with how it is _applied_ when we commit.
        }

        $statementPublisher->save();

        # !!! It seems that we execute the same logic for both single-publisher statements, and
        # !!! for 'multi-publisher' admin statements.
        #     We shouldn't.  
        #     The publisherSubtotal should NOT be incremented if the publisher is on-hold and we're reporting
        #     through an admin/agent.  (this will require changes to the commit code, I suspect).

        $self->_report("_finalize : on hold? = " . $statementPublisher->on_hold . ", admin_id? = " . $publisherData->admin_id() . ", agent_id? = " . $publisherData->agent_id(), 4);
        if (! $statementPublisher->on_hold()
         ||(! $publisherData->admin_id() && ! $publisherData->agent_id()) )
        {
            $publisherSubtotal += $statementPublisher->amount_due();
            $self->_report("_finalize : incremented publisherSubtotal to $publisherSubtotal", 4);
        }
        else
        {
            $self->_report("_finalize : DID NOT INCREMENT PUBLISHER SUBTOTAL", 4);
        }
    }
    
    # Let's see if we have any pending transactions for admins/agents
    #
    my $indirectAccounts = $self->_getIndirectAccountByPayorIndirectPublisherID($self->_payorID(), $self->_publisherID());
    
    if ($indirectAccounts)
    {
        while (my $account = $indirectAccounts->next())
        {
            my $accountID = $account->finance_account_id;
            my $statementPublisher;
            my $pendingTransactions;
            my $previousBalance;
            if ($accountID)
            {
                $previousBalance = RPS::DB::Item::FinanceAccount->CurrentBalance($accountID);
                $pendingTransactions = RPS::DB::Item::PendingTransaction->GetAccountTransactions(finance_account_id => $accountID);
                $statementPublisher = $self->_getStatementPublisherFromAccount($account);            
            }

            my $transactionSubtotal;
    
            if ($pendingTransactions)
            {
                while (my $pendingTrans = $pendingTransactions->next())
                {
                    $transactionSubtotal += $pendingTrans->amount;
                    
                    my $statementTransItem = $self->_createPublisherStatementTransactionFromPendingTransaction($pendingTrans, $account);
                    $statementTransItem->save();
                }     
            }      
     
            if ($statementPublisher)
            {
                
                my $statementPublisherSubtotal = $statementPublisher->subtotal();
                my $statementTotal = $statementPublisherSubtotal + $previousBalance + $transactionSubtotal;
                my $amountDue = 0;

                my $onHold = 0;
                my $publisherData = $self->_lookupPublisher($self->_publisherIDFromStatementPublisher($statementPublisher));
                if (RPS::Publisher::Status::kOnHold == $publisherData->status())
                {
                    $onHold = 1;
#                    $amountDue = 0;
                }
                
                # Don't mess with the publisherSubtotal for on-hold publishers.
                #
                if (! $onHold)
                {
                    if ($statementTotal > 0)
                    {
                        $amountDue = $statementTotal;   
                        # We need to figure the previous balance and transactions into the main publisher total.
                        #
                        $publisherSubtotal += ($previousBalance + $transactionSubtotal);
                    } 
                    else
                    {
                        # If the negative transaction have brought the total below or to zero, 
                        # we don't want to include those in the main publisher's total.
                        $publisherSubtotal -= $statementPublisherSubtotal;                    
                    }
                }
            
                $self->_report("_finalize : indirect account totals: transaction_subtotal $transactionSubtotal, previous_balance $previousBalance, statement_total $statementTotal, amount_due $amountDue, on_hold $onHold", 4);
     
                $statementPublisher->transaction_subtotal($transactionSubtotal);
                $statementPublisher->previous_balance($previousBalance);
                $statementPublisher->statement_total($statementTotal);
# !!! This is always 0?  So why bother?
                $statementPublisher->amount_due($amountDue);
                $statementPublisher->on_hold($onHold);   
                
                $statementPublisher->save();     
            }
                 
        }    
    }    


    # Fetch the 'main' publisher's account data.
    #
    my $publisherPreviousBalance = 0;
    my $publisherBalance = 0;
    my $publisherAppliedToBalance = 0;
    my $minPayment = 0;
    my $statementTotal = 0;
    my $transactionSubtotal = 0;

    my $publisherAccountMap = $self->_lookupPublisherAccount($self->_publisherID(), $self->_payorID());
    if ($publisherAccountMap)
    {
        $minPayment = $publisherAccountMap->min_payment;

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

        if ($pendingTransactions)
        {
            while (my $pendingTrans = $pendingTransactions->next())
            {
                $transactionSubtotal += $pendingTrans->amount;

                my $statementTransItem = $self->_createStatementTransactionFromPendingTransaction($pendingTrans);
                $statementTransItem->save();
            }
        }
    }


    $statementTotal = $publisherSubtotal + $publisherPreviousBalance + $transactionSubtotal;

    $self->_report("_finalize : final subtotals  publisherSubtotal $publisherSubtotal + publisherPreviousBalance $publisherPreviousBalance + transactionSubtotal $transactionSubtotal = statementTotal $statementTotal", 4);

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

    my $onHold = 0;
    my $publisherData = $self->_lookupPublisher($self->_publisherID());
    if (RPS::Publisher::Status::kOnHold == $publisherData->status())
    {
        $onHold = 1;
#        $amountDue = 0;
    }


    my $statement = $self->_getStatementDBItem();
    $statement->subtotal($publisherSubtotal);
    $statement->physical_subtotal($self->{_physicalSubtotal});
    $statement->digital_subtotal($self->{_digitalSubtotal});
    $statement->transaction_subtotal($transactionSubtotal);
    $statement->min_payment($minPayment);
    $statement->previous_balance($publisherPreviousBalance);

    # !!! What the hell?  These values are never changed...
    # !!! Are these deprecated somehow?
    #
    $statement->balance($publisherBalance);
    $statement->applied_to_balance($publisherAppliedToBalance);

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


    # ALL DONE!  WOO HOO!
    #
    $statement->save();  
}


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

    $self->_processPendingLicenseTransactions();
    $self->_processPendingPublisherTransactions();
}


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

    # Get all the pending transactions for licenses that may appear on this statement.
    #
    my $licenses = $self->_getLicensesWithPendingTransactions();
    while (my $license = $licenses->next())
    {
        $self->_processPendingLicenseTransaction($license);
    }
}


sub _processPendingLicenseTransaction
{
    my ($self, $license) = @_;
}

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

}

sub _processProductUnitMap
{
    my ($self, $map) = @_;

    $self->_report('_processProductUnitMap', 4);
    $self->_report($map, 4);

    foreach my $trackLicenseID (keys %$map)
    {
        my $trackLicense = $self->_getTrackLicense($trackLicenseID);

        foreach my $productID (keys %{$map->{$trackLicenseID}})
        {
            my $product = $self->_getProduct($productID);

            foreach my $issueStatRateID (keys %{$map->{$trackLicenseID}{$productID}})
            {
                foreach my $saleStatRateID (keys %{$map->{$trackLicenseID}{$productID}{$issueStatRateID}})
                {
                    my $record = $map->{$trackLicenseID}{$productID}{$issueStatRateID}{$saleStatRateID};

                    $self->_report("licenseID = $trackLicenseID, productID = $productID, issueStatRateID = $issueStatRateID, saleStatRateID = $saleStatRateID", 4);

                    $self->_processProductUnitRecord($record, $trackLicense, $product, $issueStatRateID, $saleStatRateID);
                }
            }
        }

        # !!! By having this here, we don't finish processing licenses that had no
        # !!! units moved...  This means we are not processing pending account transactions.
        #
        # But if I don't have any product units, how will I know what track licenses to check?
        #
        # I think I need to add some code in _processLicense that creates some sort of entry
        # in the $map when there are pending transactions on a track license.
        #
#        $self->_finishProcessLicense($trackLicense);
    }
}


sub _processProductUnitRecord
{
    my ($self, $record, $trackLicense, $product, $issueStatRateID, $saleStatRateID) = @_;

    if (RPS::License::LicenseType::kControlledComposition eq $trackLicense->type())
    {
        $self->_processProductUnitRecordControlledComp($record, $trackLicense, $product, $issueStatRateID, $saleStatRateID);
    }
    elsif (RPS::License::LicenseType::kPublicDomain eq $trackLicense->type())
    {
        # !!! I don't believe we ever get here.
        # !!! We catch PD licenses upstream.
        #
        $self->_processProductUnitRecordPublicDomain($record, $trackLicense, $product, $issueStatRateID, $saleStatRateID);
    }
    else
    {
        $self->_processProductUnitRecordRegular($record, $trackLicense, $product, $issueStatRateID, $saleStatRateID);
    }
}


sub _processProductUnitRecordPublicDomain
{
    my ($self, $record, $trackLicense, $product, $issueStatRateID, $saleStatRateID) = @_;

    # !!! Will probably want to note the share someplace.
}

sub _processProductUnitRecordRegular
{
    my ($self, $record, $trackLicense, $product, $issueStatRateID, $saleStatRateID) = @_;

    $self->_report("_processProductUnitRecordRegular", 4);
    $self->_report($record, 4);

    my $numSales = $record->{sales};
    my $numReturns = $record->{returns};
    my $saleIDs = $record->{saleIDs};
    my $liquidated = $record->{liquidated};
    my $previousCarryover = $record->{carryover};
    my $share = $trackLicense->share;

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

    my $duration = $self->_getTrackDuration($trackID);


    my $effectiveSales = $numSales;

    my ($effectiveRate, $baseRate, $appliedStatRateID) = $self->_calculateEffectiveRate($issueStatRateID, $saleStatRateID,
     $trackLicense->rate_basis, $trackLicense->rate_type, $trackLicense->penny_rate, $trackLicense->rate_percentage,
     $duration, $share, $trackLicense->lock_date, $trackLicense);


    # Take reserves if appropriate
    #
    my $amountReserved = 0;
    my $unitsAfterReserve;
    if ($effectiveSales > 0 && (! $self->_productIsDigital($product) || ($self->_productIsDigital($product) && $trackLicense->digital_reserves_enabled == 1)))
    {
        $unitsAfterReserve = $self->_applyDeduction($effectiveSales, $trackLicense->reserve_percentage);
        if ($unitsAfterReserve < $effectiveSales)
        {
            $amountReserved = $effectiveSales - $unitsAfterReserve;
            $effectiveSales = $unitsAfterReserve;
        }
    }


    # Apply liquidating reserves
    #
    $effectiveSales += $liquidated;


    # Some clients have some wanted to use deduct from the _net_ units (i.e. after returns)
    # I actually don't believe we have anybody doing that anymore, but it's 
    # still lurking there...
    #
    my $clientData = $self->_getClient();
    if (! $clientData->use_net_for_mechanical_deductions && $effectiveSales > 0)
    {
        $effectiveSales = $self->_standardDeductions($effectiveSales, $trackLicense);
    }

    my $effectiveUnits = $effectiveSales - $numReturns;

    if ($previousCarryover)
    {
        $effectiveUnits += $previousCarryover
    }

    if ($clientData->use_net_for_mechanical_deductions && $effectiveUnits > 0)
    {
        $effectiveUnits = $self->_standardDeductions($effectiveUnits, $trackLicense);
    }


    # WTF?   This isn't possibly correct in any way.
    #
#    my $grossUnits = $effectiveUnits;
    my $grossUnits = $numSales - $numReturns;


    my $totalOwed = Common::RSMath::round($effectiveUnits * $effectiveRate, 2);
    my $amountPaid = $totalOwed;

    # NOTE - Carryover is DISABLED!
    # !!! Can we actually, finally, blow away the tables?
    #
    my $newCarryover = 0;


    # get the statement item
    my $trackLicenseID = $self->_idFromTrackLicense($trackLicense);
    die RPS::Mechanical::Process::Exception->new("effective rate of '$effectiveRate' <= 0 for track license $trackLicenseID") unless $effectiveRate > 0;

    my $statementItem = $self->_getStatementItem($trackLicenseID, $product->product_id, $saleStatRateID, $issueStatRateID);

    my $run = $self->_getRunDBItem();
    my $hfaReportingPeriod;
    #my $hfaReportingPeriod = $self->_calculateHFAReportingPeriod($run->start_date, $run->end_date, $issueStatRateID, $saleStatRateID);



    $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();

    $self->_report(" NEW STATEMENT ITEM: ", 4);
    $self->_report($statementItem, 4);

    
    # Mark the sales that contributed to this new statementItem as having been paid.
    #
    foreach my $markSaleID (@$saleIDs)
    {
        $self->_markSalePaidWithIDs($markSaleID, $self->_idFromStatementItem($statementItem));
    }

    # Increment the running subtotals for the statement
    #
    if ($self->_productIsDigital($product))
    {
        $self->{_digitalSubtotal} += Common::RSMath::round($statementItem->amount_paid(), 2);
    }
    else
    {
        $self->{_physicalSubtotal} += Common::RSMath::round($statementItem->amount_paid(), 2);
    }


    # Now, save the reserves.
    #
    if ($amountReserved)
    {
        $self->_createReserve($trackLicenseID, $self->_reserveType(), $statementItem, $amountReserved);
    }


    # We can fetch the StatementLicense item, too, and update its subtotal
    #
    my $statementLicense = $self->_getStatementLicense($trackLicense);

    $self->_report('processProductUnitRegular', 4);
    $self->_report("license subtotal pre-adjustment = " . $statementLicense->subtotal() . ", adding in amount paid of " . $statementItem->amount_paid(), 4);
    $statementLicense->subtotal( $statementLicense->subtotal() + $statementItem->amount_paid() );

}


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


    my $percentOfSalesDeduction = 100 - $trackLicense->percentage_of_sales;
    $units = $self->_applyDeduction($units, $percentOfSalesDeduction);

#
# !!! Removing this, so Catie's head doesn't explode.
#
#    $units = $self->_applyDeduction($units, $trackLicense->packaging_deduction);
#

    $units = $self->_applyDeduction($units, $trackLicense->free_goods);

    $units = $self->_applyDeduction($units, $trackLicense->misc_deduction);

    return $units;
}




sub _processProductUnitRecordControlledComp
{
    my ($self, $record, $trackLicense, $product, $issueStatRateID, $saleStatRateID) = @_;

    # skip this if for some reason we have no share...
    #
    my $share = $trackLicense->share;
    next unless $share > 0;

    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 = $self->_getTrack($trackID);


    # !!! Irrelevant if the controlled license is min_stat.
    # !!! Actually, this seem irrelevant always.  We never use this variable again!
    my $duration = $self->_getTrackDuration($trackID);

    my $albumID = $track->album_id;
    die RPS::Mechanical::Process::Exception->new("controlled comp on track $trackID, which has no album id?") unless $albumID;
    my $ccomp = $self->_getControlledCompFromAlbumID($albumID);


    # Calculate the effective album rate
    #
    my $baseRate = $self->_calculateEffectivePerTrackRate($albumID, $issueStatRateID, $saleStatRateID, $ccomp, $trackLicense, $product->product_type_id);

    my $netSales = $numSales;

    my $amountReserved = 0;
    if ($netSales > 0 && (! $self->_productIsDigital($product) || ($self->_productIsDigital($product) && $trackLicense->digital_reserves_enabled == 1)))
    {
        my $unitsAfterReserve = $self->_applyDeduction($netSales, $ccomp->reserve_percentage);
        if ($unitsAfterReserve < $netSales)
        {
            $amountReserved = $netSales - $unitsAfterReserve;
            $netSales = $unitsAfterReserve;
        }
    }

    my $netUnits = $netSales - $numReturns;
    if ($previousCarryover)
    {
        $netUnits += $previousCarryover;
    }


    my $effectiveRate = Common::RSMath::round($baseRate * ($share / 100), 4);
    $effectiveRate = 0 if $effectiveRate < 0;

    $netUnits += $liquidated;

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

    # !!! Carryover is disabled?
    #
    my $newCarryover = 0;

    # !!! startDate and endData are the PARAMETERS passed into the run.
    #
    my $run = $self->_getRunDBItem();
    my $hfaReportingPeriod;
    #my $hfaReportingPeriod = $self->_calculateHFAReportingPeriod($run->start_date, $run->end_date, $issueStatRateID, $saleStatRateID);


    # get the statement item
    my $trackLicenseID = $self->_idFromTrackLicense($trackLicense);
    my $statementItem = $self->_getStatementItem($trackLicenseID, $product->product_id, $saleStatRateID, $issueStatRateID, $saleIDs);

    $self->_report("Creating statement item for Controlled Comp trackLicenseID $trackLicenseID", 3);
    $statementItem->net_units($netUnits);
    $statementItem->gross_units($grossUnits);
    $statementItem->net_rate($effectiveRate);
    $statementItem->total($total);
    $statementItem->amount_paid($amountPaid);
    $statementItem->base_rate($baseRate);
    $statementItem->reserved($amountReserved);
    $statementItem->sales($numSales);
    $statementItem->returns($numReturns);
    $statementItem->carryover($newCarryover);
    $statementItem->liquidated($liquidated);
    $statementItem->previous_carryover($previousCarryover);
    $statementItem->hfa_reporting_period($hfaReportingPeriod);

    # !!! Save here, or later?
    $statementItem->save();

    if ($amountReserved)
    {
        $self->_createReserve($ccomp->controlled_composition_id, RPS::DB::Item::ReserveLiquidation::kTypeControlledComposition, $statementItem, $amountReserved);
    }

    # We can fetch the StatementLicense item, too, and update its subtotal
    #
    my $statementLicense = $self->_getStatementLicense($trackLicense);

    $self->_report('processProductUnitControlledComp', 4);
    $self->_report("license subtotal pre-adjustment = " . $statementLicense->subtotal() . ", adding in amount paid of " . $statementItem->amount_paid(), 4);
    $statementLicense->subtotal( $statementLicense->subtotal() + $statementItem->amount_paid() );

}


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

    assert($controlledComp);

    $self->_report("\n\n_calculateEffectivePerTrackRate BEGIN", 4);

    my $regionID = $ccTrackLicense->region_id;

    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;

    # We want to limit the returned rate to be no more than the maximum rate for this particular
    # track, based on the rate type from the controlled comp clause.
    # We can figure out which track we're 'thinking about' by looking at ccTrackLicense.
    #
    my $maxTrackRate;

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

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

        # (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))
        {
            $self->_report("  track is a bonus track, skipping", 4);
            next;
        }

        # We need to also take public domain and 'excluded from licensing' tracks into account.
        # The latter is just a flag.
        # The former, though, involved looking for public domain licenses.
        # Which may not always be 100%.
        #
        # Need to test for 'mechanical_exempt' abstractly:  We use a different column for CA vs US mechanicals.
        #
        if ($self->_trackIsExemptFromMechanicals($track->track_id()))
        {
            $self->_report("  track is exempt from mechanicals, skipping", 4);
            next;
        }


        # Need to get the duration of each track, just in case the rate basis is full...
        #
        my $masterID = $track->master_id;
        die RPS::Mechanical::Process::Exception->new("Track has no master, cannot determine duration", $track) unless $masterID;
        my $masterData = $self->_getMaster($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) = $self->_calculateEffectiveRate(
            $issueStatRateID, 
            $saleStatRateID,
            $controlledComp->rate_basis, 
            $controlledComp->rate_type, 
            $controlledComp->penny_rate, 
            $controlledComp->rate_percentage, 
            $duration, 
            100,
            $controlledComp->lock_date,
            $ccTrackLicense);

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

        if ($track->track_id() == $ccTrackLicense->track_id())
        {
            $maxTrackRate = $trackRate;
        }
        
        

        my $hasPublicDomainFlag = 0;
        my $trackShare = 0;
        my $publicDomainShare = 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;

        $self->_report(" ccStartDate: $ccStartDate  ccEndDate: $ccEndDate", 4);

        if ($ccStartDate && $ccEndDate)
        {
            $trackLicenseList = $self->_getLicensesByTrackIDAndDate($track->track_id, $ccStartDate, $ccEndDate);
        }
        else
        {
            $trackLicenseList = $self->_getLicensesByTrackID($track->track_id);
        }
        while (my $trackLicense = $trackLicenseList->next())
        {
            # Skip inactive licenses.
            #
            if ($trackLicense->inactive())
            {
                $self->_report(" ... skipping trackLicense " . $self->_idFromTrackLicense($trackLicense) . " : inactive", 3);
                next;
            }

            # !!! For now, skip RINGTONE licenses
            #
            if ($trackLicense->type == RPS::DB::Item::TrackLicense::kRingtone)
            {
                $self->_report(" ... skipping trackLicense " . $self->_idFromTrackLicense($trackLicense) . " : RINGTONE", 3);
                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::License::LicenseType::kPublicDomain eq $trackLicense->type)
            {
                $self->_report(" -- this is a public domain license, not going to calculate rate",2);
                $hasPublicDomainFlag = 1;

                # !!! Let's try incrementing the trackShare, which it seems like we should have been
                # doing all along...
                #
                $publicDomainShare += $trackLicense->share;
#                $trackShare += $trackLicense->share;

                next;
            }

            # Skip licenses that wouldn't be applicable to the sale's product type.
            #
            if (RPS::License::LicenseType::kControlledComposition ne $trackLicense->type
             && ! $self->_productTypeMatchesLicenseType($productTypeID, $trackLicense->product_type_id()))
            {
                $self->_report(" ... skipping trackLicense " . $self->_idFromTrackLicense($trackLicense) . " : doesn't match product type $productTypeID", 3);
                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)
            {
                $self->_report(" ... skipping trackLicense " . $self->_idFromTrackLicense($trackLicense) . " : not in region $regionID", 3);
                next;
            }
            $trackShare += $trackLicense->share;
            $self->_report(" trackShare for track license " . $self->_idFromTrackLicense($trackLicense) . " = $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::License::LicenseType::kControlledComposition eq $trackLicense->type)
            {
                $self->_report(" -- this is a controlled comp license, adding share of " . $trackLicense->share . " to controlled share",3);
                $totalControlledShare += $trackLicense->share;
                next;
            }


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

            # !!! OK, I have an issue here.
            # !!! We don't take the product configuration into account.
            # !!! So, in the example album I'm looking at, there is a track that has two 100%
            # !!! licenses:  one 'all physical', and one 'all digital'.
            # !!! So now we're 200% licensed?   That seems wrong.
            # !!! But I don't think I can _average_ the licenses.  But, they don't really overlap, do they?
            # !!! We might need to feed the configuration into this calculation in order to calculate the rate correctly.
            #
            $outsideRate += $tempRate;
            $self->_report("  outsideRate now = $outsideRate", 4);
            $self->_report($trackLicense, 4);
        }

        # !!! This is, well, impossible?
        # $trackShare does not get incremented by public domain licenses, so how can it ever
        # get to 100?   Shouldn't this be _0_ ?
        # Either that, or we actually should be incrementing trackShare for public domain licenses.
        #
        if ($publicDomainShare >= 100 && $hasPublicDomainFlag)
        {
            $self->_report("  track is 100% public domain, skipping it", 3);
            next;
        }

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


        if ($publicDomainShare > 0)
        {
            $self->_report("  adding public domain share of $publicDomainShare to trackShare", 3);
            $trackShare += $publicDomainShare;
        }


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

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

            my ($tempRate)= $self->_calculateEffectiveRate(
                $issueStatRateID, 
                $saleStatRateID, 
                RPS::License::RateBasis::kSale, 
                RPS::License::RateType::kFull, 
                0, 
                100, 
                $duration, 
                $remainingShare,
                0,
                $ccTrackLicense);

            $outsideRate += $tempRate;

            $self->_report("  remainingShare = $remainingShare.  outsideRate = $outsideRate", 3);
        }
    }


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

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

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

    my $returnRate = ($effectiveAlbumRate / ($totalControlledShare / 100));
    if ($maxTrackRate && $maxTrackRate < $returnRate)
    {
        $self->_report(" !!! The calculated rate of $returnRate exceeds the maximum track rate for this track of $maxTrackRate", 3);
        $returnRate = $maxTrackRate;
    }
    $self->_report("Returning rate $returnRate \n\n", 3);

    return $returnRate;
}



sub _createReserve
{
    my ($self, $id, $type, $statementItem, $amountReserved) = @_;

    $self->_report('_createReserve', 4);

    my $statementItemID = $self->_idFromStatementItem($statementItem);
    
    # We create all the reserve buckets at once.
    #
    my %scheduleTable;
    my $schedule = RPS::DB::Item::ReserveLiquidation->GetByIDType($id, $type);
    while (my $scheduleEntry = $schedule->next())
    {
        $scheduleTable{$scheduleEntry->period()} = $scheduleEntry->percent;
    }
    
    my $createdReserve = 0;
    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 = $self->_createLicenseReserve($statementItem, $periodUnits, $i + 1);
        $newReserve->save();
        $createdReserve = 1;

        $self->_report("Create the following new license reserve:", 4);
        $self->_report($newReserve, 4);

        my $runMapItem = $self->_createLicenseReserveRunItem($newReserve, $self->_runID());
        $runMapItem->save();
    }
    
    # If for some reason we didn't create a reserve (like say the liquidation schedule is blank),
    # that's bad news so we need to bail.
    #
    if ($createdReserve == 0)
    {
        die RPS::Mechanical::Process::Exception->new("reserve was not created successfully for statement item: ", $statementItem);
    }    
    
}

sub _calculateHFAReportingPeriod
{
    my ($self, $startDate, $endDate, $issueStatRateID, $saleStatRateID) = @_;
	$self->_report("_calculateHFAReportingPeriod: $startDate, $endDate, $issueStatRateID, $saleStatRateID", 3);

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

    if ($issueStatRateID && $saleStatRateID)
    {
        my $statRates = $self->_getStatRateTable();
		$self->_report($statRates, 4);

        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;
            $self->_report("  using $startDate for startDate when calculating HFA reporting period - stat rate id $appliedRateID", 3);
        }

        # 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);
                $self->_report("  using $endDate for endDate when calculating HFA reporting period -stat rate id " . ($appliedRateID + 1), 3);
            }
        }
    }

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

    $self->_report("  HFA reporting period startDate $startDate  endDate $endDate", 3);

    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 _applyDeduction
{
    my ($self, $units, $deduction) = @_;

    return $units if ! $deduction;
    my $amountToDeduct = Common::RSMath::round($units * ($deduction / 100), 0);
    $units -= $amountToDeduct;
    return $units;
}


sub _getUPCAndISRCFromProductID
{
    my ($self, $productID) = @_;
    
    my $upc;
    my $isrc;

    # Fetch the product first.
    #
    my $product = $self->_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?
		    #
		    	        
            if ($product->parent_product_id)
            {	        
                my $albumProduct = RPS::DB::Item::Product->Lookup(product_id => $product->parent_product_id);
                if ($albumProduct)
                {
                    $upc = $albumProduct->upc_ean;
                }
            }

            my $track = $self->_getTrack($product->asset_id);
		    my $master = $self->_getMaster($track->master_id);
		    if ($master)
		    {
		        $isrc = $master->isrc;
		    }
		}
    }
    return ($upc, $isrc);
}


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

    my $item;
    if (! $self->{_statementItems}{$trackLicenseID}{$productID}{$saleStatRateID}{$issueStatRateID})
    {
        my ($upc, $isrc) = $self->_getUPCAndISRCFromProductID($productID);

        $item = $self->_createStatementItem($self->{_statementID}, $trackLicenseID, $productID, $upc, $isrc, $saleStatRateID, $issueStatRateID);

        # Need an id...
        #
        $item->save();

        $self->{_statementItems}{$trackLicenseID}{$productID}{$saleStatRateID}{$issueStatRateID} = $item;
    }

    $item = $self->{_statementItems}{$trackLicenseID}{$productID}{$saleStatRateID}{$issueStatRateID};

    if ($saleIDs)
    {
        foreach my $saleID (@$saleIDs)
        {
            my $mi = RPS::DB::Item::SaleRunMap->Lookup
            (
                sale_id => $saleID,
                run_id => $self->_runID(),
                run_type => $self->_runType(),
                statement_item_id => $self->_idFromStatementItem($item),
                status => RPS::DB::Item::SaleRunMap::kStatusPaid,
            );

            if (! $mi)
            {
                $mi = RPS::DB::Item::SaleRunMap->Create
                (
                    sale_id => $saleID,
                    run_id => $self->_runID(),
                    run_type => $self->_runType(),
                    statement_item_id => $self->_idFromStatementItem($item),
                    status => RPS::DB::Item::SaleRunMap::kStatusPaid,
                );
                $mi->save();
            }
        }
    }

    return $item;
}

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

    if (! $self->{_fileServiceMap})
    {
        my $fileList = Raptor::DB::Item::File->GetAll();
        while (my $file = $fileList->next())
        {
            $self->{_fileServiceMap}{$file->file_id} = $file->service_id;
        }
    }

    return $self->{_fileServiceMap}->{$fileID};
}


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

    if (RPS::DB::Item::TrackLicense::kControlledComposition == $trackLicense->type())
    {
        my $track = $self->_getTrack($trackLicense->track_id());
        my $cc = $self->_getControlledCompFromAlbumID($track->album_id);

        if (RPS::License::RateType::kFull == $cc->rate_type && ! $self->_getTrackDuration($trackLicense->track_id()))
        {
            return 0;
        }
    }
    else
    {
        if (RPS::License::RateType::kFull == $trackLicense->rate_type && ! $self->_getTrackDuration($trackLicense->track_id()))
        {
            return 0;
        }
    }

    return 1;
}


sub _getTrackLicense
{
    my ($self, $id) = @_;

    if (! $self->{_trackLicense}{$id})
    {
        $self->{_trackLicense}{$id} = $self->_lookupTrackLicense($id);
    }

    return $self->{_trackLicense}{$id};
}


sub _getControlledCompFromAlbumID
{
    my ($self, $albumID) = @_;
	my $albumContract = $self->_getControlledCompContractByAlbumID($albumID);
    return undef unless $albumContract;

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


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

    if (! $self->{_client})
    {
        my $clientID = Common::RSApp::GetClientID();
        $self->{_client} = Common::DB::Item::Client->Lookup(client_id => $clientID);
    }

    return $self->{_client};
}


1;
