#------------------------------------------------------------
# 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::kDateNow(),
            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::kDateNow(),
                    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::kDateNow(),
                    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::kDateNow(),
                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::kDateNow(),
                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);
        unless ( exists $licenses{$trackLicenseID} ) {
            $licenses{$trackLicenseID} = $pendingLicense;
        }
    }

    my $pendingCrossedLicenses = $self->_getCrossedLicensesWithPendingTransactions();
    while ( my $pendingCrossedLicense = $pendingCrossedLicenses->next() ) {
        my $trackLicenseID = $self->_idFromTrackLicense($pendingCrossedLicense);
        unless ( exists $licenses{$trackLicenseID} ) {
            $licenses{$trackLicenseID} = $pendingCrossedLicense;
        }
    }

    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);
    my $trackLicensePublisherID = $self->_publisherIDFromTrackLicense($trackLicense);
    die RPS::Mechanical::Process::Exception->new("effective rate of '$effectiveRate' <= 0 for track license $trackLicenseID")
      unless $effectiveRate > 0;

    my $statementItem = $self->_getStatementItem(
        $trackLicenseID, $trackLicensePublisherID, $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 $trackLicensePublisherID = $self->_publisherIDFromTrackLicense($trackLicense);
    my $statementItem = $self->_getStatementItem(
        $trackLicenseID, $trackLicensePublisherID, $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, $trackLicensePublisherID, $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,
            $trackLicensePublisherID,
            $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;
