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

package RPS::ArtistRoyalty::Process::CreateStatement;

use strict;

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

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

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

use lib '/app/tools/rps/lib';
use RPS::ArtistRoyalty::Process;
use RPS::ArtistRoyalty::Process::Exception;
use RPS::ArtistRoyalty::Utils;
use RPS::DB::Item::AlbumContract;
use RPS::DB::Item::ArtistContractCrossedMap;
use RPS::DB::Item::ArtistContractTermReserve;
use RPS::DB::Item::ArtistContractTermReserveRun;
use RPS::DB::Item::ArtistPayee;
use RPS::DB::Item::ArtistPayeeAccount;
use RPS::DB::Item::ArtistRoyaltyAlbum;
use RPS::DB::Item::ArtistRoyaltyAlbumBalanceAccount;
use RPS::DB::Item::ArtistRoyaltyExpenseItem;
use RPS::DB::Item::ArtistRoyaltyIncomeItem;
use RPS::DB::Item::ArtistRoyaltyLicenseIncomeItem;
use RPS::DB::Item::ArtistRoyaltyRun;
use RPS::DB::Item::ArtistRoyaltyRunMissedSaleLog;
use RPS::DB::Item::ArtistRoyaltyTransaction;
use RPS::DB::Item::Channel;
use RPS::DB::Item::ContractLevelLicenseIncomeBalanceAccount;
use RPS::DB::Item::ContractRateType;
use RPS::DB::Item::DistributionFee;
use RPS::DB::Item::Expense;
use RPS::DB::Item::ExpenseArtistPayeeMap;
use RPS::DB::Item::ExpenseName;
use RPS::DB::Item::ExpenseType;
use RPS::DB::Item::FinanceAccount;
use RPS::DB::Item::Format;
use RPS::DB::Item::IncomeSource;
use RPS::DB::Item::LicenseIncome;
use RPS::DB::Item::LicenseIncomeArtistPayeeMap;
use RPS::DB::Item::ArtistContractLicenseIncome;
use RPS::DB::Item::NewArtistContractTerm;
use RPS::DB::Item::Payor;
use RPS::DB::Item::PendingTransaction;
use RPS::DB::Item::Price;
use RPS::DB::Item::PriceLevel;
use RPS::DB::Item::Product;
use RPS::DB::Item::ProductPrice;
use RPS::DB::Item::ProductType;
use RPS::DB::Item::Region;
use RPS::DB::Item::RegionCountryMap;
use RPS::DB::Item::ReserveArtistPayeeMap;
use RPS::DB::Item::ReserveLiquidation;
use RPS::DB::Item::SaleArtistPayeeMap;
use RPS::DB::Item::SaleProductType;
use RPS::DB::Item::SaleRunMap;
use RPS::DB::Item::ServiceFormatChannelMap;
use RPS::DB::Item::Track;
use RPS::DB::Item::TrackContract;
use RPS::File::Sale;
use RPS::Statement::Status;

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

use constant kDefaultLogLevel => 2;

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

my %gData;


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

    $self->_report("Testing... " . $self->{_statementID} . " initialized", 3);

    # Invoke the inherited method to set up logging.
    #
    $self->SUPER::_init(%args);

    # The statementID is required.
    # AND the statement should exist in the database already.
    #
    assert($args{statementID});

    $self->{_statementID} = $args{statementID};


    # Grab the 'run' data, so we can fetch the payor_id.
    #
    my $run = $self->_getRunDBItem();
    $self->{_payorID} = $run->payor_id;
    $self->{_runID}   = $run->artist_royalty_run_id;

    # Grab the statement data, so we can fetch the payee_id.
    #
    my $statement = $self->_getStatementDBItem();
    $self->{_payeeID} = $statement->payee_id;

    $self->_report("CreateStatement process for statement " . $self->{_statementID} . " initialized", 3);
    return $self;
}


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

    my $statement = $self->_getStatementDBItem();
    my $runID = $statement->artist_royalty_run_id;
    my $run = RPS::DB::Item::ArtistRoyaltyRun->Lookup(artist_royalty_run_id => $runID);
    return $run;
}


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

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


sub _statementID
{
    my ($self) = @_;
    return $self->{_statementID};
}


sub _payorID
{
    my ($self) = @_;
    return $self->{_payorID};
}


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


sub _payeeID
{
    my ($self) = @_;
    return $self->{_payeeID};
}

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


    my $payeeID = $self->_payeeID();

    $self->_report("Creating statement for payee: $payeeID", 2);

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

    $statement->status(RPS::Statement::Status::kRunning);
    $statement->save();

    # Get the array of sale IDs we plan on processing.
    #
    my $saleIDs = $self->_getSaleIDs();
    $self->_report("Fetched sales. Count = " . scalar(@$saleIDs), 2);
    $self->_processSales($saleIDs);

    $self->_processLicenseIncomeSales();

    my $reserveIDs = $self->_getReserveIDs();
    $self->_report("Fetched reserves. Count = " . scalar(@$reserveIDs), 2);
    $self->_processReserves($reserveIDs);

    $self->_fillInData();

    $self->_processExpenses();

    $self->_generateStatements();

    $self->_finalize();


    $self->_report("Done, marking statement as complete", 2);
    $statement->status(RPS::Statement::Status::kComplete);
    $statement->save();

    return 0;
}


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

    my $exception = $args{exception};

    my $errorString;
    if (ref $exception && $exception->isa('Common::Exception'))
    {
        $errorString = $exception->errorMessage();
    }
    else
    {
        $errorString = $exception;
    }

    my $statement = $self->_getStatementDBItem();
    $statement->error($errorString);
    $statement->status(RPS::Statement::Status::kError);
    $statement->save();
}


sub _processSales
{
    my ($self, $saleIDs) = @_;

    $self->_report("Processing sales", 2);
    foreach my $saleID (@$saleIDs)
    {
        my $sale = Raptor::DB::Item::Sale->Lookup(sale_id => $saleID);

        $self->_report("--------------------------------------------------------", 3);
        $self->_report("  sale_id " . $sale->sale_id, 3);
        $self->_report("--------------------------------------------------------", 3);

        # I want to catch any exceptions, so I can display the naughty sale id.
        #
        eval {
            $self->_processSale($sale);
        };

        if ($@)
        {
            $self->_report("CAUGHT AN EXCEPTION");
            $self->_report($@);
            if (ref $@ && $@->isa('RPS::ArtistRoyalty::Process::SaleException'))
            {
            # !!! This is a neat idea, but we don't appear to have actually implemented it.
            #
                # We can log these.
                #
                $self->_logSaleException($@);
            }
            else
            {
                # Something else (bad) happened.
                # Re-throw the original exception
                #
                die($@);
            }
        }
    }
}


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

    # Read the sales to process from the SaleArtistPayeeMap table
    #

    my $payeeID = $self->_payeeID();
    my $runID   = $self->_runID();
    my $run     = $self->_getRunDBItem();

    $self->_report("fetching sale map for payee $payeeID and run $runID", 3);

    # !!! Going to change this.
    # !!! We need to get sales from multiple places (two different map tables).
    # !!! - Remember, we need to apply the same contraints we used to iterate over 
    # !!!   the sale table the first time (when we build the product_artist_payee_map table).
    my @saleIDs;

    my $idList = Raptor::DB::Item::Sale->GetUnprocessedRoyaltySaleIDsUsingProductArtistPayeeMap($payeeID, $runID, $run->ending_sale_date());
    push @saleIDs, @$idList;

    $idList = RPS::DB::Item::SaleArtistPayeeMap->GetPayeeMappedSaleIDs($payeeID, $runID);
    push @saleIDs, @$idList;

    return \@saleIDs;
}


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

    # Read the sales to process from the ReserveArtistPayeeMap table
    #

    my $payeeID = $self->_payeeID();
    my $runID   = $self->_runID();

    $self->_report("fetching reserve map for payee $payeeID and run $runID", 3);

    return RPS::DB::Item::ReserveArtistPayeeMap->GetPayeeMappedReserveIDs($payeeID, $runID);
}


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

    $self->_report("processSale", 4);
    $self->_report($sale, 4);

    my %badProducts;


    # Fetch the product this sale references.
    #
    # !!!
    # There will be some sales that do not have a product_id.
    # Namely, master use and sync sales.
    # These will just have a track_id.
    #
    # This means that I cannot use product_id as a key into the statement item table.
    # I am going to have to use album_id/track_id
    #
    my $productID = $sale->product_id;


    # We need the album_id, and the track_id if this is a track sale.
    #
    my $albumID;
    my $trackID;

    my $missingProductPriceFlag = 0;
    if ($productID)
    {
        my $product = RPS::DB::Item::Product->Lookup(product_id => $productID);
        if (! $product)
        {
            logMissedSale($self->_runID(), $sale, 'Invalid product id');
            $self->_report("skipping sale_id " . $sale->sale_id . " : product $productID cannot be found", 3);
            my $mi = RPS::DB::Item::SaleRunMap->Create
            (
                sale_id => $sale->sale_id,
                run_id => $self->_runID(),
                run_type => RPS::DB::Item::SaleRunMap::kRunTypeArtistRoyalty,
                status => RPS::DB::Item::SaleRunMap::kStatusBadProductID,
            );
            $mi->save();
            next;
        }

        # Skip inactive products.
        #
        if (RPS::DB::Item::Product::kProductStatusActive != $product->product_status_id)
        {
            $self->_report("skipping sale_id " . $sale->sale_id . " : product $productID is not active", 3);
            ($albumID, $trackID) = getAlbumTrackFromProduct($product);
            my $details = "Product $productID, type " . _idToProductType($product->product_type_id);
            logMissedSale($self->_runID(), $sale, "Product inactive", productID => $productID, albumID => $albumID, trackID => $trackID, details => $details);
            my $mi = RPS::DB::Item::SaleRunMap->Create
            (
                sale_id => $sale->sale_id,
                run_id => $self->_runID(),
                run_type => RPS::DB::Item::SaleRunMap::kRunTypeArtistRoyalty,
                status => RPS::DB::Item::SaleRunMap::kStatusInactiveProduct,
            );
            $mi->save();
            next;
        }

        # !!! Sanity check.
        # !!! If we don't have any entries in the product_price table for this product,
        # !!! then we might not be able to pay on it... We won't know for certain
        # !!! until we've fetched the contracts.
        #
        # If this is a track product... well, we don't have entries for track products.
        # Have to use the album product entry instead.
        #
        my $productIDToCheck = $productID;
        if (RPS::DB::Item::Product::kProductTypeDigitalTrack == $product->product_type_id)
        {
            my $trackID = $product->asset_id;
            my $trackData = RPS::DB::Item::Track->Lookup(track_id => $trackID);
            my $albumID = $trackData->album_id;

            # Find the digital album product id
            #
            my $digitalAlbumProductList = RPS::DB::Item::Product->GetProductsByAlbumID($albumID, RPS::DB::Item::Product::kProductTypeDigital);
            if ($digitalAlbumProductList && $digitalAlbumProductList->size() > 0)
            {
                my $digitalAlbumProduct = $digitalAlbumProductList->next();
                $productIDToCheck = $digitalAlbumProduct->product_id;
            }
        }
        my $productPrices = RPS::DB::Item::ProductPrice->GetByProductID($productIDToCheck);
        if (! $productPrices || 0 == $productPrices->size())
        {
            $self->_report("missing product price for product id: $productIDToCheck", 3);
            $missingProductPriceFlag = 1;
        }

        # (XXX) Need to take this a step further - is there the correct price in the table?
        #

        ($albumID, $trackID) = getAlbumTrackFromProduct($product);
    }
    else
    {
        # XXX This field name will probably change
        #
        $trackID = $sale->lic_track_id;
        if (! $trackID)
        {
            logMissedSale($self->_runID(), $sale, 'No track id or product id');
            $self->_report("skipping sale_id " . $sale->sale_id . " : no product id or lic_track_id ", 3);
            my $mi = RPS::DB::Item::SaleRunMap->Create
            (
                sale_id => $sale->sale_id,
                run_id => $self->_runID(),
                run_type => RPS::DB::Item::SaleRunMap::kRunTypeArtistRoyalty,
                status => RPS::DB::Item::SaleRunMap::kStatusNoLicTrackOrProductID,
            );
            $mi->save();
            next;
        }
        my $track = RPS::DB::Item::Track->Lookup(track_id => $trackID);
        $albumID = $track->album_id;
    }

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



    my $countryCode = $sale->country_code;
    my $regionID;
    my $channelID = $sale->channel;
    my $priceLevelID = $sale->price_level;
    
    my $productType = $sale->product_type;

    if ($productID)
    {
        my $product = RPS::DB::Item::Product->Lookup(product_id => $productID);
        if ($product)  
        {
            my $realProductType = _translate_product_type_id_to_product_type($product->product_type_id);
            if ($realProductType)
            {
                $productType = $realProductType;
            }            
        }  
    }
       
    my $incomeSourceID = $self->_determineIncomeSourceID($productType, $sale->format_type);

    if (! $incomeSourceID)
    {
        # !!! going to skip this sale
        #
        $self->_report("skipping sale_id " . $sale->sale_id . " :  could not find income source id", 3);
        logMissedSale($self->_runID(), $sale, "No matching income source",
         details => 'No income source id matches product_type ' . $self->_idToSaleProductType($sale->product_type) . ', format type ' . $self->_idToFormatType($sale->format_type),
         productID => $productID, albumID => $albumID, trackID => $trackID);
        my $mi = RPS::DB::Item::SaleRunMap->Create
        (
            sale_id => $sale->sale_id,
            run_id => $self->_runID(),
            run_type => RPS::DB::Item::SaleRunMap::kRunTypeArtistRoyalty,
            status => RPS::DB::Item::SaleRunMap::kStatusNoMatchingIncomeSourceID,
        );
        $mi->save();
        next;
    }

    #my $clientID = Common::RSApp::GetClientID();
    #if (123 == $clientID) # WELK
    #{
    #    if (RPS::DB::Item::IncomeSource::kIncomeSourceDigitalStream == $incomeSourceID
    #     || RPS::DB::Item::IncomeSource::kIncomeSourceDigitalTethered == $incomeSourceID)
    #    {
    #        logMissedSale($self->_runID(), $sale, "Skipping streams and tethered",
    #         details => 'Skipping stream and tethered download sales per client request - product type ' . $self-#>_idToSaleProductType($sale->product_type) . ', format_type ' . $self->_idToFormatType($sale->format_type),
    #        productID => $productID, albumID => $albumID, trackID => $trackID);
    #        my $mi = RPS::DB::Item::SaleRunMap->Create
    #        (
    #            sale_id => $sale->sale_id,
    #            run_id => $self->_runID(),
    #            run_type => RPS::DB::Item::SaleRunMap::kRunTypeArtistRoyalty,
    #            status => RPS::DB::Item::SaleRunMap::kStatusSkipped,
    #        );
    #        $mi->save();
    #        next;
    #    }
    #}


    my $units = $sale->units;
    my $numSales = $sale->sales;
    my $numReturns = $sale->returns;
    my $averagePrice = $sale->average_price;
    my $wholesalePrice = $sale->wholesale_price;
    my $retailPrice = $sale->retail_price;


    my $isDigitalFlag;
    my $productTypeID;
    if ($productID)
    {
        my $product = RPS::DB::Item::Product->Lookup(product_id => $productID);
        $productTypeID = $product->product_type_id;
        if (RPS::DB::Item::Product::kProductTypeDigital == $productTypeID
         || RPS::DB::Item::Product::kProductTypeDigitalTrack == $productTypeID)
        {
            $isDigitalFlag = 1;
            if ($units < 0)
            {
                $numReturns = (-1 * $units);
            }
            else
            {
                $numSales = $units;
            }

            # Digital sales don't set the price_level_id.  I will have to hard-code it.
            #
            if (RPS::DB::Item::Product::kProductTypeDigitalTrack == $productTypeID)
            {
                $priceLevelID = RPS::DB::Item::PriceLevel::kPriceLevelTrackDownload;
            }
            if (RPS::DB::Item::Product::kProductTypeDigital == $productTypeID)
            {
                $priceLevelID = RPS::DB::Item::PriceLevel::kPriceLevelAlbumDownload;
            }
        }
        else
        {
            $units = $numSales - $numReturns;

            # make sure there is a price level.  If the sale didn't specify one, use
            # the default from the product.
            #
            if (! $priceLevelID)
            {
                $priceLevelID = $product->default_price_level_id;
            }
        }


        # There ought to be an entry in the product_price table... if not, we might not be able to pay.
        # (XXX) But, until I know which term we're using, I won't be sure if it even matters...


    }
    else
    {
        # !!! If there isn't a productID, then this is some sort of 'weird' sale, like a master use.
        # Seems like all of these have different ways of encoding their units.
        # I'll default to using units, unless there aren't any.
        #
        if (RPS::DB::Item::IncomeSource::kIncomeSourceMasterLicense == $incomeSourceID
         || RPS::DB::Item::IncomeSource::kIncomeSourceSyncLicense == $incomeSourceID)
        {
            $isDigitalFlag = 1;
        }

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


    # !!! Bottom line - after this code block net units will be $numSales - $numReturns.



    # This routine returns an array of hashes that look like this:
    #{
    #    artistContract => $artistContract,
    #    albumID => $albumID,
    #    trackID => $trackContract->track_id,
    #};

    # Make note of crossing here, too.
    #
    # !!! Can I specify the list of payor ids here?
    # !!! It would be convenient if this did not return contracts for inactive payees...
    #
    # We're going to check contracts for all payors for a match.
    # This way, we can suppres the "No matching contracts" message if there
    # is a match for a different payor.
    my $allPayorIDs = getAllPayorIDs();
    my $contractList = RPS::ArtistRoyalty::Utils::GetMatchingContracts($albumID, $trackID, $allPayorIDs);

    # Need to loop through this twice - the first time so I can sanity check the product_price situation.
    #
    # !!! Need to extend this test to see whether we've got the _correct_ price_level_id in the product_price table.
    my $skipThisSale = 0;
    if (0 == scalar @$contractList)
    {
        logMissedSale($self->_runID(), $sale, 'No matching contracts', productID => $productID, albumID => $albumID, trackID => $trackID);
        $self->_report("...no matching contracts found, skipping sale " . $sale->sale_id, 3);
        my $mi = RPS::DB::Item::SaleRunMap->Create
        (
            sale_id => $sale->sale_id,
            run_id => $self->_runID(),
            run_type => RPS::DB::Item::SaleRunMap::kRunTypeArtistRoyalty,
            status => RPS::DB::Item::SaleRunMap::kStatusNoMatchingContracts,
        );
        $mi->save();
        next;
    }



    foreach my $contractHash (@$contractList)
    {
        my $contract = $contractHash->{artistContract};

        # Since $contractList now contains contracts for all payors,
        # we need to filter out all of them except for the ones for this payor.
        #
        if ($contract->payor_id != $self->_payorID())
        {
            next;
        }

        # Same thing for payees.
        #
        if ($contract->artist_payee_id != $self->_payeeID())
        {
            next;
        }


        my $term = RPS::ArtistRoyalty::Utils::GetMatchingTerm($contract, $countryCode, $channelID, $incomeSourceID, $priceLevelID, $wholesalePrice, $retailPrice);
        if (! $term)
        {
            next;
        }
        my $rateType = $term->contract_rate_type_id;


        if (RPS::DB::Item::ContractRateType::kRateTypePercentRetail == $rateType
         || RPS::DB::Item::ContractRateType::kRateTypePercentWholesale == $rateType
         || RPS::DB::Item::ContractRateType::kRateTypePercentDocumentRetail == $rateType
         || RPS::DB::Item::ContractRateType::kRateTypePercentDocumentWholesale == $rateType
         || RPS::DB::Item::ContractRateType::kRateTypePercentPPD == $rateType)
        {


            # First, let's make sure we have a price level id.
            #
            if (! $priceLevelID)
            {
                my $productType = _idToProductType($productTypeID);
                logMissedSale($self->_runID(), $sale, "No price level", details => "No default price level set for  $productType product",
                 productID => $productID, albumID => $albumID, trackID => $trackID, contract => $contract,
                );
                $self->_report("!!! skipping this sale : matched term " . Dumper($term) . " but didn't have a price level for product type id $productTypeID", 3);
                #$badProducts{$productID} = 1;
                $skipThisSale = 1;
                last;
            }


            # Do we have an entry in the product price table?
            # This doesn't matter if we're using document price.
            if (RPS::DB::Item::ContractRateType::kRateTypePercentDocumentRetail != $rateType
             && RPS::DB::Item::ContractRateType::kRateTypePercentDocumentWholesale != $rateType)
            {
                my $productPrice = $self->_getPrice($priceLevelID, $productID);
                if (! $productPrice)
                {
                    logMissedSale($self->_runID(), $sale, "No product price", details => "No product price entry for price level " . $self->_idToPriceLevel($priceLevelID),
                     productID => $productID, albumID => $albumID, trackID => $trackID, contract => $contract,
                    );
                    $self->_report("!!! skipping this sale : matched term " . Dumper($term) . " but didn't have a product price entry", 3);

                    $badProducts{$productID} = 1;
                    $skipThisSale = 1;
                    last;
                }
            }
        }
    }


    if ($skipThisSale)
    {
        $self->_report(" ...skipping sale", 3);
        my $mi = RPS::DB::Item::SaleRunMap->Create
        (
            sale_id => $sale->sale_id,
            run_id => $self->_runID(),
            run_type => RPS::DB::Item::SaleRunMap::kRunTypeArtistRoyalty,
            status => RPS::DB::Item::SaleRunMap::kStatusNoProductPrice,
        );
        $mi->save();
        next;
    }


    # Build a hash that contains the contract ids that have already paid on this
    # sale, and any associated track ids.
    #
    my $previousContracts = $self->_getPaidContractIDsForSale($sale);

    # We'll keep track of the original track id before we iterate through the contracts.
    # This is because we want to set the track_id to whatever value any _track_ contracts
    # have set up, so it displays properly... And we'll need to set it back to the original
    # value afterwards.
    #
    my $origTrackID = $trackID;

    my $termsMatched = 0;
    foreach my $contractHash (@$contractList)
    {

        my $contract = $contractHash->{artistContract};

        # Since $contractList now contains contracts for all payors,
        # we need to filter out all of them except for the ones for this payor.
        #
        if ($contract->payor_id != $self->_payorID())
        {
            next;
        }

        # And also the ones for other payees...
        #
        if ($contract->artist_payee_id != $self->_payeeID())
        {
            next;
        }

        $self->_report(" contractHash :", 4);
        $self->_report($contractHash, 4);
        $self->_report(" previousContracts", 4);
        $self->_report($previousContracts, 4);

        # Restore the original track_id, in case a previous contract modified it.
        #
        $trackID = $origTrackID;

        # If this contract was attached via a TrackContract, then the hash will have a trackID.
        # We'll use that.
        # There will also now be a 'prorateTrackCount', to be used for prorating.
        #
        my $prorateTrackCount = 1;
        if ($contractHash->{trackID})
        {
            $trackID = $contractHash->{trackID};
            if ($contractHash->{prorateTrackCount})
            {
                $prorateTrackCount = $contractHash->{prorateTrackCount};
            }
        }


        # Now we can check to see whether we've already paid this sale for this contract for this track.
        #
        my $checkTrackID = 0;
        $checkTrackID = $contractHash->{trackID} if $contractHash->{trackID};

        $self->_report("checking track id " . $checkTrackID . " for paid contracts", 3);

        if ($previousContracts->{$contract->artist_contract_id}{$checkTrackID})
        {
            $self->_report("skipping contract " . $contract->artist_contract_id . " : already paid", 3);
            next;
        }
        if ($origTrackID && $previousContracts->{$contract->artist_contract_id}{$origTrackID})
        {
            $self->_report("skipping contract " . $contract->artist_contract_id . " : already paid", 3);
            next;
        }
        
        # Now we will make sure that the sale date is in the correct range for this contract, 
        # if one has been set.
        #my $saleDate = $sale->date_begin;
        my $saleOutOfRange = 0;
        if ($contract->term_start)
        {
            if ($sale->date_end lt $contract->term_start)
            {
                $saleOutOfRange = 1;
            }
            
        }
        
        if ($contract->term_end)
        {
            if ($sale->date_end gt $contract->term_end)
            {
                $saleOutOfRange = 1;
            }            
        }
        
        if ($saleOutOfRange == 1)
        {
            $self->_report("skipping sale for contract " . $contract->artist_contract_id . " - date out of term range", 3);
            logMissedSale($self->_runID(), $sale, "Date out of range", details => 'Sale date does not occur within term date range of contract', productID => $productID, trackID => $trackID, albumID => $albumID, contract => $contract );           
            next; 
        }
        
        
        my $term = RPS::ArtistRoyalty::Utils::GetMatchingTerm($contract, $countryCode, $channelID, $incomeSourceID, $priceLevelID, $wholesalePrice, $retailPrice);

        if (! $term)
        {
            # Let's grab the income source so that we can modify the message based on its format
            #
            my $incomeSource = RPS::DB::Item::IncomeSource->Lookup(income_source_id => $incomeSourceID);
            my $details = "No term found for country code $countryCode, income source " . $incomeSource->description;

            if ($incomeSource->format == RPS::DB::Item::IncomeSource::kFormatPhysical)
            {
                $details .= ", price level " . $self->_idToPriceLevel($priceLevelID) . ", channel " . $self->_idToChannel($channelID);
            }

            $self->_report("skipping sale for contract " . $contract->artist_contract_id . " - no matching terms", 3);
            logMissedSale($self->_runID(), $sale, "No matching term", details => $details, productID => $productID, trackID => $trackID, albumID => $albumID, contract => $contract );

            # !!! Do I want to add this to the SaleRunMap?
            next;
        }

        # Get region id from the term
        #
        $regionID = $term->region_id;
        $regionID = 0 unless $regionID;

        # !!! I need to deal with the default term in a special way.... So I will need to know whether
        # !!! this term is the default.
        # !!! I think the 'priority=0' will suffice.
        #

        # Total revenue is calculated differently for physical and digital sales.
        # And is only relevant for net revenue terms.
        # 
        # Actually, we need the revenue number for the non-payable rate type now, too.
        my $totalRevenue = $sale->total_revenue * $sale->conversion_rate;
        my $rateTypeID = $term->contract_rate_type_id;
        if (RPS::DB::Item::ContractRateType::kRateTypePercentRevenue == $rateTypeID || RPS::DB::Item::ContractRateType::kRateTypeNonPayable == $rateTypeID)
        {
            if ($isDigitalFlag)
            {
                # !!! For Instinct, and other labels that do this, we'll need to
                # determine the mechanical stat rate, and _subtract_ that from the totalRevenue.
                # This would be the place to do that.
                # (!!!) Is this just for track sales?   Probably not.  So, for an album sale, I will have to
                # (!!!) determine the stat rate on all tracks on the album (except for bonus tracks?)
                #
                my $price = $sale->price;


                # Only do this for US sales... and only for downloads.
                #
                if ($self->_isDownload($incomeSourceID) && 'US' eq $sale->country_code() && $self->_subtractStatRateFromNetRevenue())
                {
                    my $statRatePrice = $self->_determineStatRatePrice($sale);
                    $self->_report("  price: $price statRatePrice: $statRatePrice new price " . ($price - $statRatePrice), 3);
                    $price -= $statRatePrice;
                }
                $totalRevenue = $price * $units * $sale->conversion_rate;
            }

            # Apply the distribution fee reduction, if there is one.
            #
            my $distributionFee = $self->_getDistributionFeeFromSale($sale);
            if ($distributionFee)
            {
                $totalRevenue = Common::RSMath::round($totalRevenue * ((100 - $distributionFee)/100), 2);
            }
        }


        # !!!!!!! I _think_ I can get the price here, and stuff it into $averagePrice.
        # !!!!!!! Rather than doing it in _fillInData.

        # Average price is only relevant for percentage of average price deals - for the
        # rest, I want to ignore whatever junk may be in the sale table.
        #
        if (RPS::DB::Item::ContractRateType::kRateTypePercentAverage != $rateTypeID)
        {
            $averagePrice = 0;
        }

        $self->_report(" totalRevenue $totalRevenue rateTypeID $rateTypeID isDigital $isDigitalFlag units $units sales $numSales returns $numReturns price " . $sale->price . " conversion_rate " . $sale->conversion_rate . " TERM:", 4);
        $self->_report($term, 4);


        # Apply the prorate to the rate (if this is an album sale)
        #
        my $rate = $term->rate;
        if (RPS::File::Sale::TYPE_ALBUM eq $sale->product_type
         || $self->_isAlbumIncomeSource($incomeSourceID))
        {
            $self->_report("prorateTrackCount: $prorateTrackCount : pre proration rate = $rate", 3);
            $rate /= $prorateTrackCount;
            $self->_report("   post proration rate = $rate", 3);
        }


        # !!! Note that we're passing in priceLevelID.
        # !!! Rather than 'price' itself.
        # !!! EXCEPT that this will not work for stupid master use sales, so we need to pass price, too.
        #
        $termsMatched++;

        # !!!
        # I want to be able to log _exactly_ which statement item(s) this sale will land on.
        # But... I don't know that yet!
        #
        $self->_createStatementData($numSales, $numReturns, $rate, $totalRevenue, $averagePrice, $term, $albumID, $trackID, $productID, $incomeSourceID, $regionID, $channelID, $priceLevelID, $contract, $rateTypeID, $sale->price, $sale->sale_id);
    }


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

    my @badProductIDs = sort keys %badProducts;
    foreach my $badProductID (@badProductIDs)
    {
        $self->_report("!!! product_id $badProductID missing product_price map entry, skipping associated sales", 3);
    }

}


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

    my $payeeID = $self->_payeeID();
    my $runID = $self->_runID();

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

    my $mapItems = RPS::DB::Item::LicenseIncomeArtistPayeeMap->GetPayeeMappedLicenseIncome($payeeID, $runID);
    while (my $mapItem = $mapItems->next())
    {
        my $licenseIncomeData = RPS::DB::Item::LicenseIncome->Lookup(license_income_id => $mapItem->license_income_id);
        my $artistContractID = $mapItem->artist_contract_id;

        my $albumID = $licenseIncomeData->album_id;
        my $trackID = $licenseIncomeData->track_id;          
        
        # Let's make sure that the link between contract and album/track is still active.
        # Note that the user can enter only the album, track and album, or neither,
        # so we have to be careful when testing.
        if ($albumID && ! $trackID)
        {
            my $albumContract = RPS::DB::Item::AlbumContract->Lookup(album_id => $albumID, artist_contract_id => $artistContractID);
            if (! $albumContract)
            {
                $self->_report("skipping license income " . $mapItem->license_income_id . " - cannot find the AlbumContract", 4);
                next;
            }
            if ($albumContract->status != RPS::DB::Item::AlbumContract::kStatusActive)
            {
                $self->_report("skipping license income " . $mapItem->license_income_id . " - AlbumContract is not active", 4);
                next;
            }
        } 
        elsif ($trackID && $albumID)
        {
            my $trackContract = RPS::DB::Item::TrackContract->Lookup(track_id => $trackID, artist_contract_id => $artistContractID);
            my $albumContract = RPS::DB::Item::AlbumContract->Lookup(album_id => $albumID, artist_contract_id => $artistContractID);
            if (! $trackContract && ! $albumContract)
            {
                $self->_report("skipping license income " . $mapItem->license_income_id . " - cannot find the TrackContract or AlbumContract", 4);
                next;
            }
            my $trackContractStatus;
            my $albumContractStatus;
            if ($trackContract)
            {
                $trackContractStatus = $trackContract->status;
            }
            if ($albumContract)
            {
                $albumContractStatus = $albumContract->status;
            }            
            if ($trackContractStatus != RPS::DB::Item::TrackContract::kStatusActive && $albumContractStatus != RPS::DB::Item::AlbumContract::kStatusActive)
            {
                $self->_report("skipping license income " . $mapItem->license_income_id . " - TrackContract and AlbumContract are not active", 4);
                next;
            }            
        }                         
        
        $albumID = 0 unless $albumID;
        $trackID = 0 unless $trackID;

        # fetch the contract's 'term' data, to see whether we _really_ pay or not.
        #
        my $term = RPS::DB::Item::ArtistContractLicenseIncome->Lookup(
         artist_contract_id => $artistContractID,
         license_income_type_id => $licenseIncomeData->license_income_type_id,
         inactive => 0,
        );

        next unless $term;
        next if $term->inactive;

        $self->_report("+++ matching contract " . $artistContractID . " with this term: ", 4);
        $self->_report($term, 4);

        my $rate = $term->percent;
        $rate = 0 unless $rate;

        $gData
        {$artistContractID}
        {$albumID}
        {$trackID}
        {0}
        {licenseIncomeID}
        {$licenseIncomeData->license_income_id}
        {$rate}
        {lineitem}
        {units} += $licenseIncomeData->units;

        $gData
        {$artistContractID}
        {$albumID}
        {$trackID}
        {0}
        {licenseIncomeID}
        {$licenseIncomeData->license_income_id}
        {$rate}
        {lineitem}
        {revenue} += $licenseIncomeData->revenue;
    }
}


sub _processReserves
{
    my ($self, $reserveIDs) = @_;

    my $payorID = $self->_payorID();
    my $payeeID = $self->_payeeID();

    $self->_report("Processing reserves", 2);
    foreach my $reserveID (@$reserveIDs)
    {
        my $reserve = RPS::DB::Item::ArtistContractTermReserve->Lookup(artist_contract_term_reserve_id => $reserveID);
        my $termID = $reserve->artist_contract_term_id;
        my $term = RPS::DB::Item::NewArtistContractTerm->Lookup(artist_contract_term_id => $termID);
        my $contract = RPS::DB::Item::NewArtistContract->Lookup(artist_contract_id => $term->artist_contract_id);

        # !!! So, we need to see if the Track/AlbumContract linkage still exists, and if it is still 'active'.
        #
        if ($reserve->track_id)
        {
            my $trackContract = RPS::DB::Item::TrackContract->Lookup(track_id => $reserve->track_id, artist_contract_id => $contract->artist_contract_id);
            # !!! Should I _require_ that this exist?
            if (! $trackContract)
            {
                $self->_report("skipping reserve" . $reserve->artist_contract_term_reserve_id . " - cannot find the TrackContract", 3);
                next;
            }
            if ($trackContract->status != RPS::DB::Item::TrackContract::kStatusActive)
            {
                $self->_report("skipping reserve" . $reserve->artist_contract_term_reserve_id . " - TrackContract is not active", 3);
                next;
            }
        }
        elsif ($reserve->album_id)
        {
            my $albumContract = RPS::DB::Item::AlbumContract->Lookup(album_id => $reserve->album_id, artist_contract_id => $contract->artist_contract_id);
            # !!! Should I _require_ that this exist?
            if (! $albumContract)
            {
                $self->_report("skipping reserve" . $reserve->artist_contract_term_reserve_id . " - cannot find the AlbumContract", 3);
                next;
            }
            if ($albumContract->status != RPS::DB::Item::AlbumContract::kStatusActive)
            {
                $self->_report("skipping reserve" . $reserve->artist_contract_term_reserve_id . " - AlbumContract is not active", 3);
                next;
            }
        }


        # We've determined that this reserve 'belongs' to this run, so mark it.
        #
        my $runMapItem = RPS::DB::Item::ArtistContractTermReserveRun->Create
        (
            artist_contract_term_reserve_id => $reserve->artist_contract_term_reserve_id,
            artist_royalty_run_id => $self->_runID(),
        );
        $runMapItem->save();


        # If the period == 1, we liquidate
        #
        my $period = $reserve->periods_remaining;
        if (1 != $period)
        {
            next;
        }

        # Figure out whether we have a single album contract, or a list of track contracts.
        # We'll need to know the track ids and the proration amount.
        #
#        my $trackProrationData = _getTrackProrationData($reserve->product_id, $term);


        my $product = RPS::DB::Item::Product->Lookup(product_id => $reserve->product_id);
        if (! $product)
        {
            die RPS::ArtistRoyalty::Process::Exception->new("reserve references a non-existent product:", $reserve);
        }

        if ($reserve->revenue_based)
        {
            my $reserveRevenue = $reserve->revenue;

            if ($reserveRevenue)
            {
                # Fetch the contract term and the contract, so we can get the
                # rest of the relevant info.
                #
#                my $albumID = $product->album_id;
#                my $albumID = _getAlbumIDFromProduct($product);
                my $albumID = $reserve->album_id;
                my $productID = $reserve->product_id;
                my $incomeSourceID = $reserve->income_source_id;
                my $regionID = $reserve->region_id;
                my $channelID = $reserve->channel_id;
                my $priceLevelID = $reserve->price_level_id;


                # Price is meaningless unless this is a percent average term.
                # Force it to '0' so everything will hash correctly.
                #
                my $rateTypeID = $term->contract_rate_type_id;
                my $price = $reserve->price;


                # !!! I want to trust the price and price level unless for some reason price is 0...
                #
                if (0 == $price)
                {
                    ($price, $priceLevelID) = _determinePrice($rateTypeID, $price, $priceLevelID, $productID);
                }

                #my $clientID = Common::RSApp::GetClientID();
                #if (123 == $clientID) # WELK
                #{
                    if (0 == $price)
                    {
                        if (RPS::DB::Item::ContractRateType::kRateTypePercentRetail == $rateTypeID
                         || RPS::DB::Item::ContractRateType::kRateTypePercentWholesale == $rateTypeID
                         || RPS::DB::Item::ContractRateType::kRateTypePercentPPD == $rateTypeID)
                        {
                            # un-mark this reserve!
                            #
                            $self->_report("SKIPSKIPSKIP : Skipping reserve " . $reserve->artist_contract_term_reserve_id . " product:$productID", 3);
                
                            # Delete the entry from the map table, so we don't touch it.
                            #
                            $runMapItem->delete();
                            next;
                        }
                    }
                #}

                # !!!!!! We should _keep_ price - But, perhaps if price is 0 then we need to determine it from
                # !!!!!! the price level id.
                #
#                $price = 0 unless $price > 0 && RPS::DB::Item::ContractRateType::kRateTypePercentAverage == $rateTypeID;


#                foreach my $trackProration (@$trackProrationData)
#                {
#                    my $trackID = $trackProration->{track_id};
                    my $trackID = $reserve->track_id;

                    $trackID = 0 unless $trackID;

                    # Round the rate, to make sure we are always hashing on the same things
                    # (i.e. 40 vs 40.0000)
                    #

                    # !!! My theory now is that this proration is not necessary.
                    # !!! It seems to be getting applied more that is necessary.
                    # !!! In other words, every time we liquidate reserves we reduce _again_!
                    #
#                    my $count = $trackProration->{proration_count};
                    my $count = 1;
                    if (! $count)
                    {
                        $self->_report("WARNING - 0 proration count", 2);
                        $count = 1;
                    }
                    my $rate = Common::RSMath::round($reserve->effective_rate / $count, 4);

                    # !!! price and rate will need to be rounded consistently to hash properly
                    #
                    $price = Common::RSMath::round($price, 4);
                    $rate = Common::RSMath::round($rate, 4);

                    $gData
                        {$term->artist_contract_id}
                        {$albumID}
                        {$trackID}
                        {$productID}
                        {incomeSourceID}
                        {$incomeSourceID}
                        {$regionID}
                        {$channelID}
                        {$priceLevelID}
                        {$term->artist_contract_term_id}
                        {$price}
                        {$rate}
                        {lineitem}{revenueLiquidated} += $reserveRevenue;
#                }
            }
        }
        else
        {
            my $reserveUnits = $reserve->units;

            if ($reserveUnits)
            {
                # Fetch the contract term and the contract, so we can get the
                # rest of the relevant info.
                #
#                my $albumID = _getAlbumIDFromProduct($product);
#                my $albumID = $product->album_id;
                my $albumID = $reserve->album_id;
                my $productID = $reserve->product_id;
                my $incomeSourceID = $reserve->income_source_id;
                my $regionID = $reserve->region_id;
                my $channelID = $reserve->channel_id;
                my $priceLevelID = $reserve->price_level_id;


                my $rateTypeID = $term->contract_rate_type_id;
                my $price = $reserve->price;

                # !!! I want to trust the price and price level unless for some reason price is 0...
                #
                if (0 == $price)
                {
                    ($price, $priceLevelID) = _determinePrice($rateTypeID, $price, $priceLevelID, $productID);
                }

                #my $clientID = Common::RSApp::GetClientID();
                #if (123 == $clientID) # WELK
                #{
                    if (0 == $price)
                    {
                        if (RPS::DB::Item::ContractRateType::kRateTypePercentRetail == $rateTypeID
                         || RPS::DB::Item::ContractRateType::kRateTypePercentWholesale == $rateTypeID
                         || RPS::DB::Item::ContractRateType::kRateTypePercentPPD == $rateTypeID)
                        {
                            # un-mark this reserve!
                            #
                            $self->_report("SKIPSKIPSKIP : Skipping reserve " . $reserve->artist_contract_term_reserve_id . " product:$productID", 3);
                
                            # Delete the entry from the map table, so we don't touch it.
                            #
                            $runMapItem->delete();
                            next;
                        }
                    }
                #}
                # !!! Not sure WHY we are setting this to 0.  Doesn't seem to help.
                # !!! So I am going to comment it out.
#                $price = 0 unless $price > 0 && RPS::DB::Item::ContractRateType::kRateTypePercentAverage == $rateTypeID;



                # !!! The big question: Why am I doing track proration here?
                # !!! Haven't we _already_ prorated this?

#                foreach my $trackProration (@$trackProrationData)
#                {
#                    my $trackID = $trackProration->{track_id};
                    my $trackID = $reserve->track_id;
                    $trackID = 0 unless $trackID;

                    # Round the rate, to make sure we are always hashing on the same things
                    # (i.e. 40 vs 40.0000)
                    #
#                    my $count = $trackProration->{proration_count};
                    my $count = 1;
                    if (! $count)
                    {
                        $self->_report("WARNING - 0 proration count", 2);
                        $count = 1;
                    }
                    my $rate = Common::RSMath::round($reserve->effective_rate / $count, 4);

                    # !!! price and rate will need to be rounded consistently to hash properly
                    #
                    $price = Common::RSMath::round($price, 4);
                    $rate = Common::RSMath::round($rate, 4);

                    $self->_report(" price $price  rate $rate", 3);
                    $gData
                        {$term->artist_contract_id}
                        {$albumID}
                        {$trackID}
                        {$productID}
                        {incomeSourceID}
                        {$incomeSourceID}
                        {$regionID}
                        {$channelID}
                        {$priceLevelID}
                        {$term->artist_contract_term_id}
                        {$price}
                        {$rate}
                        {lineitem}{unitsLiquidated} += $reserveUnits;
#                }
            }
        }
    }
}


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

    # Ok, so we now have all the units accumulated.  We can now apply rates, deductions, etc.
    #
    $self->_report("--- calculating royalties", 2);
    $self->_report("DATA: ", 4);
    $self->_report(\%gData, 4);

    my $artistPayeeID = $self->_payeeID();

    foreach my $contractID (keys %gData)
    {
        $self->_report("_fillInData contractID $contractID", 3);
        my $defaultTerm = RPS::DB::Item::NewArtistContractTerm->GetDefault($contractID);
        my $defaultTermID = 0;
        my $defaultNetRevenueRate = 100;
        if ($defaultTerm)
        {
            $defaultTermID = $defaultTerm->artist_contract_term_id;
            $defaultNetRevenueRate = $defaultTerm->rate;
        }

        my $albumIDHash = $gData{$contractID};
        foreach my $albumID (keys %$albumIDHash)
        {
            my $trackIDHash = $albumIDHash->{$albumID};
            foreach my $trackID (keys %$trackIDHash)
            {
                my $productIDHash = $trackIDHash->{$trackID};
                foreach my $productID (keys %$productIDHash)
                {
                    my $incomeSourceIDHash = $productIDHash->{$productID}{incomeSourceID};
                    foreach my $incomeSourceID (keys %$incomeSourceIDHash)
                    {
                        my $regionIDHash = $incomeSourceIDHash->{$incomeSourceID};
                        foreach my $regionID (keys %$regionIDHash)
                        {
                            my $channelIDHash = $regionIDHash->{$regionID};
                            foreach my $channelID (keys %$channelIDHash)
                            {
                                my $priceLevelIDHash = $channelIDHash->{$channelID};
                                foreach my $priceLevelID (keys %$priceLevelIDHash)
                                {
                                    my $artistContractTermIDHash = $priceLevelIDHash->{$priceLevelID};
                                    foreach my $artistContractTermID (keys %$artistContractTermIDHash)
                                    {
                                        my $averagePriceHash = $artistContractTermIDHash->{$artistContractTermID};
                                        foreach my $price (keys %$averagePriceHash)
                                        {
                                            my $baseRateHash = $averagePriceHash->{$price};
                                            foreach my $baseRate (keys %$baseRateHash)
                                            {
                                            $self->_report("_fillInData: $artistPayeeID, $contractID, $albumID, $trackID, $productID, $incomeSourceID, $regionID, $channelID, $priceLevelID, $artistContractTermID, $price", 3);
                                            my $lineItemRecord = $baseRateHash->{$baseRate}{lineitem};

                                            my $sales =             $lineItemRecord->{sales};
                                            my $returns =           $lineItemRecord->{returns};
                                            my $unitsLiquidated =   $lineItemRecord->{unitsLiquidated};
                                            my $dollarsLiquidated = $lineItemRecord->{revenueLiquidated};
                                            my $totalRevenue =      $lineItemRecord->{revenue};


                                            my $term = RPS::DB::Item::NewArtistContractTerm->LookupActive(artist_contract_term_id => $artistContractTermID);

                                            my $contract = RPS::DB::Item::NewArtistContract->Lookup(artist_contract_id => $contractID);


                                            my $rateType = $term->contract_rate_type_id;
#                                                my $baseRate = $term->rate;
                                            my $rateReduction = $term->rate_reduction;
                                            my $percentageOfSales = $term->percentage_of_sales;
                                            my $packagingDeduction = $term->packaging_deduction;
                                            my $freeGoods = $term->free_goods_deduction;
                                            my $reservePercentage = $contract->reserve_rate;


                                            my $unitsReserved;
                                            my $dollarsReserved;
                                            my $netUnits;
                                            my $netRevenue;
                                            my $netRate;
                                            my $total = 0;
#                                                my $price;

                                            my $actualPriceLevelID = $priceLevelID;
                                            my $usingDefaultRate = 0;
                                            if ($artistContractTermID == $defaultTermID)
                                            {
                                                $usingDefaultRate = 1;
                                            }


                                            if (RPS::DB::Item::ContractRateType::kRateTypeNonPayable == $rateType)
                                            {                
                                                # Do we need to do anything here?
                                                #
                                            }                            
                                            elsif (RPS::DB::Item::ContractRateType::kRateTypePercentRevenue == $rateType)
                                            {
                                                $totalRevenue = Common::RSMath::round($totalRevenue, 2);

                                                ($dollarsReserved, $netRevenue, $netRate, $total) =
                                                    $self->_netRevenueBased($productID, $totalRevenue, $baseRate, $rateReduction,
                                                        $percentageOfSales, $packagingDeduction, $freeGoods, $dollarsLiquidated,
                                                        $reservePercentage);

                                                # jpk - we do want to display the net units (customer request)
                                                #
                                                $netUnits = $sales - $returns;



                                            }
                                            else
                                            {
                                                # If the user hasn't entered a price... well, we're hosed.
                                                # We should probably skip this _sale_.
                                                # (XXX) Trouble is, we're long past the point where that
                                                # would be practical.  We'll have to do this in a pre-processing step.
                                                #
                                                if (! $price)
                                                {
                                                    $self->_report("term that could not be matched to a price: ", 3);
                                                    $self->_report($term, 3);
                                                    next;
                                                }

                                                ($unitsReserved, $netUnits, $netRate, $total) =
                                                 $self->_priceBased($productID, $price, $baseRate, $rateReduction, $percentageOfSales, $packagingDeduction,
                                                 $freeGoods, $sales, $returns, $unitsLiquidated, $reservePercentage, $rateType);

                                                # jpk - we don't want to display the revenue data, since it's
                                                # meaningless (and misleading).
                                                # It would be nice if the sales records didn't have any data in these
                                                # columns, but we can't rely on that.
                                                #
#                                                    $lineItemRecord->{revenue} = 0;
                                            }


                                            # Create the statement and statement items
                                            # !!!
                                            #
                                            # Let's not create actual statement items : Instead, store these calculated stuff
                                            # in the record, and we'll loop through it again later.
                                            #
                                            $lineItemRecord->{unitsReserved}        = $unitsReserved;
                                            $lineItemRecord->{dollarsReserved}      = $dollarsReserved;
                                            $lineItemRecord->{netUnits}             = $netUnits;
                                            $lineItemRecord->{netRevenue}           = $netRevenue;
                                            $lineItemRecord->{netRate}              = $netRate;
                                            $lineItemRecord->{total}                = $total;
                                            $lineItemRecord->{price}                = $price;
                                            $lineItemRecord->{baseRate}             = $baseRate;
                                            $lineItemRecord->{rateReduction}        = $rateReduction;
                                            $lineItemRecord->{percentageOfSales}    = $percentageOfSales;
                                            $lineItemRecord->{packagingDeduction}   = $packagingDeduction;
                                            $lineItemRecord->{freeGoods}            = $freeGoods;
                                            $lineItemRecord->{reservePercentage}    = $reservePercentage;
                                            $lineItemRecord->{actualPriceLevelID}   = $actualPriceLevelID;

                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}


sub _netRevenueBased
{
    my ($self, $productID, $totalRevenue, $baseRate, $rateReduction, $percentageOfSales, $packagingDeduction, $freeGoods, $dollarsLiquidated, $reservePercentage) = @_;

    my $dollarsReserved;
    my $netRevenue;
    my $netRate;
    my $total;


    $self->_report("_netRevenueBased: productID $productID totalRevenue $totalRevenue  br $baseRate  rr $rateReduction  pos $percentageOfSales pd $packagingDeduction fg $freeGoods dl $dollarsLiquidated rp $reservePercentage", 3);

    # Take reserves before deductions.
    #
    $netRevenue = $totalRevenue;
    if ($netRevenue > 0 && _canTakeReserves($productID) && $reservePercentage)
    {
        $dollarsReserved = Common::RSMath::round($netRevenue * ($reservePercentage / 100), 2);
        $netRevenue -= $dollarsReserved;

        $self->_report("  took reserves. dollars reserved: $dollarsReserved  new net revenue $netRevenue", 3);
    }


    # Add in liquidated units
    #
    $netRevenue += $dollarsLiquidated;
    $self->_report("  added in liquidated units of $dollarsLiquidated, netRevenue $netRevenue", 3);


    if ($netRevenue > 0)
    {
        $netRevenue = Common::RSMath::round($netRevenue * ((100 - $freeGoods)/100), 2) if $freeGoods;
        $self->_report("   free goods deduction of " . (1 - $freeGoods) . "yields $netRevenue", 3);
    }


    # Apply rate deductions
    # !!! Do these apply at all, really?
    #
    $netRate = $baseRate;

    $netRate = ($netRate * ($percentageOfSales / 100)) if $percentageOfSales;
    $self->_report("   percentage of sales of $percentageOfSales yields rate of $netRate", 3);

    $netRate = ($netRate * ($rateReduction / 100)) if $rateReduction;
    $self->_report("   rate reduction of $rateReduction yields rate of $netRate", 3);

    $netRate = ($netRate * ((100 - $packagingDeduction) / 100)) if $packagingDeduction;
    $self->_report("   packaging reduction of " . (1 - $packagingDeduction) . " yields rate of $netRate", 3);



    $total = Common::RSMath::round((($netRate / 100) * $netRevenue), 2);
    $self->_report("   TOTAL: $total  (($netRate / 100) * $netRevenue)", 3);


    return ($dollarsReserved, $netRevenue, $netRate, $total);
}

sub _priceBased
{
    my ($self, $productID, $price, $baseRate, $rateReduction, $percentageOfSales, $packagingDeduction, $freeGoods, $sales, $returns, $unitsLiquidated, $reservePercentage, $rateType) = @_;

    my $unitsReserved;
    my $netUnits;
    my $netRate;
    my $total;

    $self->_report("_priceBased: productID $productID price $price br $baseRate  rr $rateReduction  pos $percentageOfSales pd $packagingDeduction fg $freeGoods sales $sales returns $returns ul $unitsLiquidated rp $reservePercentage", 3);


    # According to Catie, I need to apply the percentageOfSales deduction FIRST
    #
    if ($sales > 0 && $percentageOfSales > 0)
    {
        $sales = Common::RSMath::round($sales * ($percentageOfSales / 100), 0);
        $self->_report("  after percentage of sales deduction of $percentageOfSales, sales = $sales", 3);
    }
#    $netRate = ($netRate * ($percentageOfSales / 100)) if $percentageOfSales;
#    _report("  after pos , netRate = $netRate", 2);

    # Take reserves before deductions.
    #
    if ($sales > 0 && _canTakeReserves($productID) && $reservePercentage)
    {
        $unitsReserved = Common::RSMath::round($sales * ($reservePercentage / 100), 0);
        if ($unitsReserved > $sales)
        {
            $unitsReserved = $sales;
        }
        $sales -= $unitsReserved;
    }
    $self->_report("  after reserves, sales = $sales ", 3);

    # Add in liquidated units
    #
    $netUnits = $sales;
    $netUnits += $unitsLiquidated;
    $self->_report("  netUnits = $netUnits after liquidating $unitsLiquidated units", 3);


    # Take free goods next
    #
    if ($netUnits > 0 && $freeGoods)
    {
        $netUnits = Common::RSMath::round($netUnits * ((100 - $freeGoods)/100), 0);
        $self->_report("  after free goods, netUnits = $netUnits", 3);
    }


    $netUnits -= $returns;
    $self->_report("  netUnits = $netUnits after subtracting returns of $returns", 3);


    # Apply rate deductions
    #
    $netRate = $price;
    $self->_report("  netRate = $netRate", 3);
    if (RPS::DB::Item::ContractRateType::kRateTypeFixed == $rateType)
    {
        $netRate = ($netRate * $baseRate);
    }
    else
    {
        $netRate = ($netRate * ($baseRate / 100));
    }
    $self->_report("  after base rate applied, netRate = $netRate", 3);

# !!! Sigh  -  NOW they tell me that percentageOfSales is _NOT_ a rate reduction, but a unit reduction.
#    $netRate = ($netRate * ($percentageOfSales / 100)) if $percentageOfSales;
#    _report("  after pos , netRate = $netRate", 2);

    $netRate = ($netRate * ($rateReduction / 100)) if $rateReduction;
    $self->_report("  after rate reduction applied, netRate = $netRate", 3);

    $netRate = ($netRate * ((100 - $packagingDeduction) / 100)) if $packagingDeduction;
    $self->_report("  after packaging applied , netRate = $netRate", 3);



    $total = Common::RSMath::round(($netRate * $netUnits), 2);
    $self->_report("  total = $total", 3);

    return ($unitsReserved, $netUnits, $netRate, $total);
}


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

    # Read the expenses to process from the ExpenseArtistPayeeMap table
    #

    my $payeeID = $self->_payeeID();
    my $runID   = $self->_runID();

    $self->_report("fetching expense map for payee $payeeID and run $runID", 3);

    return RPS::DB::Item::ExpenseArtistPayeeMap->GetPayeeMappedSaleIDs($payeeID, $runID);
}


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

    my $payeeID = $self->_payeeID();
    my $runID = $self->_runID();

    $self->_report("--- processing expenses", 2);

    my $mapItems = RPS::DB::Item::ExpenseArtistPayeeMap->GetPayeeMappedExpenses($payeeID, $runID);
    while (my $mapItem = $mapItems->next())
    {
        my $expense = RPS::DB::Item::Expense->Lookup(expense_id => $mapItem->expense_id);

        my $name = $self->_getExpenseName($expense);

        # !!! the 'preProcess' flag seems hokey.
        # I'd rather assign this to the default term id key.
        #
        my $termID = 0;
#        my $termRate = 100;
        if ($expense->pre_process)
        {
            # Get the default net rate term for this artist contract.
            #
            # ingnore active flags per case #12004
            my $term = RPS::DB::Item::NewArtistContractTerm->GetDefault($mapItem->artist_contract_id);
            assert($term, "FATAL ERROR: Missing default term for artist contract: " . $mapItem->artist_contract_id);
            $termID = $term->artist_contract_term_id;
#            $termRate = $term->rate;
        }


        # For the 'Bob' terms, that have the default net rate applied as well, we'll
        # need to fill in the 'termRate' column, and use that in our calculations.
        # (XXX) Which means that we'll need to fetch the default rate?  Or should we do the
        # (XXX) math later?  No, we should do the math here.
        #
        my $expenseRate = $expense->percent;
#        my $netRate = ($termRate * ($expenseRate / 100));
        my $total = Common::RSMath::round(($expense->amount * ($expenseRate / 100)), 4);

        my $expenseData =
        {
            description => $name,
            cost => $expense->amount,
            rate => $expenseRate,
#            termRate => $termRate,
            total => $total,
            expenseID => $expense->expense_id,
            memo => $expense->memo,
        };

        $expenseRate = Common::RSMath::round($expenseRate, 4);

        push @{$gData
            {$mapItem->artist_contract_id}
            {$mapItem->album_id}
            {$mapItem->track_id}
            {0}
            {incomeSourceID}
            {0}
            {0}
            {0}
            {0}
            {$termID}
            {0}
            {$expenseRate}
            {expenses}
            }, $expenseData;
    }

}


sub _getExpenseName
{
    my ($self, $expense) = @_;

    my $expenseType = RPS::DB::Item::ExpenseType->Lookup(expense_type_id => $expense->expense_type_id);
    my $expenseName = RPS::DB::Item::ExpenseName->Lookup(expense_name_id => $expenseType->expense_name_id);
    return $expenseName->name;
}


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

    $self->_report("--- generating statements", 3);
    # _report("data: " . Dumper(\%gData), 4);

    my $payorID = $self->_payorID();
    my $artistPayeeID = $self->_payeeID();


    my $artistPayeeData = RPS::DB::Item::ArtistPayee->Lookup(artist_payee_id => $artistPayeeID);
    if (RPS::DB::Item::ArtistPayee::kStatusInactive == $artistPayeeData->status)
    {
        die RPS::ArtistRoyalty::Process::Exception->new("artist payee $artistPayeeID is inactive - abort");
    }

    # We need to also fetch any _album balances_, so we can generate statements for those as well.
    #
    my $albumBalanceHash = $self->_getAlbumBalances();

    foreach my $albumID (keys %$albumBalanceHash)
    {
        my $artistContractIDHash = $albumBalanceHash->{$albumID};
        foreach my $artistContractID (keys %$artistContractIDHash)
        {
            if (! exists $gData{$artistContractID}{$albumID})
            {
                $gData{$artistContractID}{$albumID} = {};
            }
        }
    }


    # Get the payee account info
    #
    my $artistPayeeAccount = RPS::DB::Item::ArtistPayeeAccount->Lookup(artist_payee_id => $artistPayeeID, payor_id => $payorID);
    my $minPayment = 0;
    if ($artistPayeeAccount)
    {
        $minPayment = $artistPayeeAccount->min_payment;
    }

    my $statementID = $self->_statementID();

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

    my $statementLicenseIncomeSubtotal = 0;

    my $contractLevelLicenseIncomeSubtotal = 0;
    # !!! need previous balance, and total (which includes the previous balance)
    #

    my $statementSubtotal = 0;
    my $crossIncome = 0;
    my $crossExpense = 0;
    my $crossBalance = 0;
    my $crossTotal = 0;
    my $unCrossedTotal = 0;

    # Get the current balance for this account.
    # !!! I'd like to make an entry in this hash if this guy has _pending transactions_, too.
    #
    my $currentBalance;
    my $hasPendingTransactions;
    my $pendingTransactions;
    my $payeePreviousBalance;
    my $payeePendingTransactions;

    my $account = RPS::DB::Item::ArtistPayeeAccount->Lookup(artist_payee_id => $artistPayeeID, payor_id => $payorID);
    if ($account)
    {
        if ($account->finance_account_id)
        {
            $currentBalance = RPS::DB::Item::FinanceAccount->CurrentBalance($account->finance_account_id);
            $pendingTransactions = RPS::DB::Item::PendingTransaction->GetAccountTransactions(finance_account_id => $account->finance_account_id);
        }
        if ($currentBalance != 0 || ($pendingTransactions && $pendingTransactions->size() > 0))
        {
            $payeePreviousBalance = $currentBalance;
            if ($pendingTransactions && $pendingTransactions->size() > 0)
            {
                $payeePendingTransactions = $pendingTransactions;
            }
        }
    }


    foreach my $contractID (keys %gData)
    {
        # Fetch the default term, so we can tell later on when we're
        # dealing with items in the default rate pool.
        #
        my $defaultTerm = RPS::DB::Item::NewArtistContractTerm->GetDefault($contractID);
        my $defaultTermID = 0;
        my $defaultNetRevenueRate = 100;
        if ($defaultTerm)
        {
            $defaultTermID = $defaultTerm->artist_contract_term_id;
            $defaultNetRevenueRate = $defaultTerm->rate;
        }

        my $albumIDHash = $gData{$contractID};
        foreach my $albumID (keys %$albumIDHash)
        {
            # Create the 'album' grouping table item.
            #
            $self->_report("creating artist_royalty_album entry for album $albumID contract $contractID  ",4);

            my $albumItem = RPS::DB::Item::ArtistRoyaltyAlbum->Create
            (
                artist_royalty_statement_id => $statementID,
                album_id => $albumID,
                artist_contract_id => $contractID,
                default_net_revenue_rate => $defaultNetRevenueRate,
            );
            # !!! Don't save this if the album_id == 0...
            #
            if ($albumID > 0)
            {
                $albumItem->save();
            }

            my $albumItemID = $albumItem->artist_royalty_album_id;
            $albumItemID = 0 unless $albumItemID;

            my $albumUnitLevelIncome = 0;
            my $albumNetRevenueIncome = 0;
            my $albumNetRevenueExpenses = 0;
            my $albumRecoupableExpenses = 0;
            my $albumLicenseIncomeSubtotal = 0;


            my $trackIDHash = $albumIDHash->{$albumID};
            foreach my $trackID (%$trackIDHash)
            {
                my $productIDHash = $trackIDHash->{$trackID};
                foreach my $productID (keys %$productIDHash)
                {
                    my $upc;
                    if ($productID)
                    {
                        my $product = RPS::DB::Item::Product->Lookup(product_id => $productID);
                        $upc = $product->upc_ean;
                    }

                    my $incomeSourceIDHash = $productIDHash->{$productID}{incomeSourceID};
                    foreach my $incomeSourceID (keys %$incomeSourceIDHash)
                    {
                        my $regionIDHash = $incomeSourceIDHash->{$incomeSourceID};
                        foreach my $regionID (keys %$regionIDHash)
                        {
                            my $channelIDHash = $regionIDHash->{$regionID};
                            foreach my $channelID (keys %$channelIDHash)
                            {
                                my $priceLevelIDHash = $channelIDHash->{$channelID};
                                foreach my $priceLevelID (keys %$priceLevelIDHash)
                                {
                                    my $artistContractTermIDHash = $priceLevelIDHash->{$priceLevelID};
                                    foreach my $artistContractTermID (keys %$artistContractTermIDHash)
                                    {
                                        # Fetch the term. We'll need stuff out of there.
                                        #
                                        my $term = RPS::DB::Item::NewArtistContractTerm->LookupActive(artist_contract_term_id => $artistContractTermID);

                                        my $averagePriceHash = $artistContractTermIDHash->{$artistContractTermID};
                                        foreach my $averagePrice (keys %$averagePriceHash)
                                        {
                                            my $baseRateHash = $averagePriceHash->{$averagePrice};
                                            foreach my $baseRate (keys %$baseRateHash)
                                            {
                                            my $lineItem= $baseRateHash->{$baseRate}{lineitem};

                                            my $usesDefaultRateFlag = 0;
                                            if ($artistContractTermID == $defaultTermID)
                                            {
                                                $usesDefaultRateFlag = 1;
                                            }

                                            if ($lineItem)
                                            {
                                                if (RPS::DB::Item::ContractRateType::kRateTypePercentRevenue == $term->contract_rate_type_id)
                                                {
                                                    $albumNetRevenueIncome += $lineItem->{total};
                                                }
                                                else
                                                {
                                                    $albumUnitLevelIncome += $lineItem->{total};
                                                }

                                                my $incomeStatementItem = RPS::DB::Item::ArtistRoyaltyIncomeItem->Create
                                                (
                                                    artist_royalty_album_id => $albumItem->artist_royalty_album_id,
                                                    album_id => $albumID,
                                                    artist_contract_term_id => $artistContractTermID,

                                                    income_source_id => $incomeSourceID,
                                                    region_id => $regionID,
                                                    channel_id => $channelID,
                                                    price_level_id => $priceLevelID,

                                                    contract_rate_type_id => $term->contract_rate_type_id,
                                                    rate => $baseRate,
                                                    rate_reduction => $lineItem->{rateReduction},
                                                    percentage_of_sales => $lineItem->{percentageOfSales},
                                                    packaging_deduction => $lineItem->{packagingDeduction},
                                                    free_goods_deduction => $lineItem->{freeGoods},
                                                    units_reserved => $lineItem->{unitsReserved},
                                                    revenue_reserved => $lineItem->{dollarsReserved},
                                                    net_units => $lineItem->{netUnits},
                                                    net_revenue => $lineItem->{netRevenue},
                                                    net_rate => $lineItem->{netRate},
                                                    upc_ean => $upc,
                                                    total => $lineItem->{total},
                                                    price => $lineItem->{price},
                                                    sales => $lineItem->{sales},
                                                    returns => $lineItem->{returns},
                                                    revenue => $lineItem->{revenue},
                                                    units_liquidated => $lineItem->{unitsLiquidated},
                                                    revenue_liquidated => $lineItem->{revenueLiquidated},
                                                    uses_default_net_rate => $usesDefaultRateFlag,
                                                );
                                                $incomeStatementItem->track_id($trackID) if $trackID;
                                                $incomeStatementItem->save();

                                                # Log this sale/line item mapping.
                                                #
                                                if ($lineItem->{saleIDs})
                                                {
                                                    my $statementItemID = $incomeStatementItem->artist_royalty_income_item_id;
                                                    foreach my $saleID (keys %{$lineItem->{saleIDs}})
                                                    {
                                                        my $mi = RPS::DB::Item::SaleRunMap->Create
                                                        (
                                                            sale_id => $saleID,
                                                            run_id => $self->_runID(),
                                                            statement_item_id => $statementItemID,
                                                            run_type => RPS::DB::Item::SaleRunMap::kRunTypeArtistRoyalty,
                                                            status => RPS::DB::Item::SaleRunMap::kStatusPaid,
                                                        );
                                                        $mi->save();
                                                    }
                                                }


                                                # !!! Create a reserve item here.
                                                #
                                                if ($incomeStatementItem->units_reserved > 0)
                                                {
                                                    $self->_createUnitReserve($incomeStatementItem, $productID);
                                                }
                                                elsif ($incomeStatementItem->revenue_reserved > 0)
                                                {
                                                    $self->_createRevenueReserve($incomeStatementItem, $productID);
                                                }
                                            }

                                            my $expenses = $baseRateHash->{$baseRate}{expenses};
                                            foreach my $expenseData (@$expenses)
                                            {
                                                my $expenseStatementItem = RPS::DB::Item::ArtistRoyaltyExpenseItem->Create
                                                (
                                                    expense_id => $expenseData->{expenseID},
                                                    uses_default_net_rate => $usesDefaultRateFlag,
                                                    artist_royalty_album_id => $albumItem->artist_royalty_album_id,
                                                    expense_name => $expenseData->{description},
                                                    term_rate => $expenseData->{termRate},
                                                    cost => $expenseData->{cost},
                                                    rate => $expenseData->{rate},
                                                    total => $expenseData->{total},
                                                    memo  => $expenseData->{memo},
                                                );
                                                $expenseStatementItem->track_id($trackID) if $trackID;
                                                $expenseStatementItem->save();
                                                if ($artistContractTermID == $defaultTermID)
                                                {
                                                    $albumNetRevenueExpenses += $expenseData->{total};
                                                }
                                                else
                                                {
                                                    $albumRecoupableExpenses += $expenseData->{total};
                                                }
                                            }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }

                    # Now work on the license income stuff.
                    #
                    my $licenseIncomeIDHash = $productIDHash->{$productID}{licenseIncomeID};
                    foreach my $licenseIncomeID (keys %$licenseIncomeIDHash)
                    {
                        my $rateHash = $licenseIncomeIDHash->{$licenseIncomeID};
                        foreach my $rate (keys %$rateHash)
                        {
                            my $lineData = $rateHash->{$rate}{lineitem};

                            my $units = $lineData->{units};
                            my $revenue = $lineData->{revenue};
                            my $salesHash = $lineData->{sales};

                            my $licenseIncomeData = RPS::DB::Item::LicenseIncome->Lookup(license_income_id => $licenseIncomeID);
                            # Calculate the effective revenue
                            #
                            my $effectiveRevenue = Common::RSMath::round($revenue * ($rate / 100), 2);


                            # Create the proper statement item.
                            #
                            my $liItem = RPS::DB::Item::ArtistRoyaltyLicenseIncomeItem->Create
                            (
                                license_income_id => $licenseIncomeID,
                                artist_royalty_statement_id => $statementID,
                                album_id => $albumID,
                                track_id => $trackID,
                                artist_contract_id => $contractID,
                                license_income_type_id => $licenseIncomeData->license_income_type_id,
                                rate => $rate,
                                units => $units,
                                revenue => $revenue,
                                net_revenue => $effectiveRevenue,
                                memo => $licenseIncomeData->memo,
                            );
                            $liItem->save();


                            # Make an entry in the map table.
                            #
                            my $mi = RPS::DB::Item::SaleRunMap->Create
                            (
                                sale_id => $licenseIncomeData->sale_id,
                                run_id => $self->_runID(),
                                statement_item_id => $liItem->artist_royalty_license_income_item_id,
                                run_type => RPS::DB::Item::SaleRunMap::kRunTypeArtistRoyalty,
                                status => RPS::DB::Item::SaleRunMap::kStatusPaid,
                            );
                            $mi->save();


                            # Update the various related subtotals.
                            # !!! Album id might be 0, if this was just contract matched
                            #
                            $albumLicenseIncomeSubtotal += $effectiveRevenue;
                            if (0 == $albumID)
                            {
                                $contractLevelLicenseIncomeSubtotal += $effectiveRevenue;
                            }
                        }
                    }
                }
            }


            # jpk - sure hope all of this adds up...
            #
            my $adjustedNetRevenueExpensesSubtotal = Common::RSMath::round($albumNetRevenueExpenses * ($defaultNetRevenueRate / 100), 4);


            my $albumTotalIncome = $albumUnitLevelIncome + $albumNetRevenueIncome;
            my $albumTotalExpenses = $albumRecoupableExpenses + $adjustedNetRevenueExpensesSubtotal;
 
            my $albumPreviousBalance = $self->_getAlbumPreviousBalance($payorID, $artistPayeeID, $albumID, $contractID);
            my $albumTotal = $albumTotalIncome - $albumTotalExpenses + $albumPreviousBalance + $albumLicenseIncomeSubtotal;

            $albumItem->unit_level_income($albumUnitLevelIncome);
            $albumItem->net_revenue_income($albumNetRevenueIncome);
            $albumItem->net_revenue_expenses($albumNetRevenueExpenses);
            $albumItem->net_revenue_expenses_subtotal($adjustedNetRevenueExpensesSubtotal);
            $albumItem->recoupable_expenses($albumRecoupableExpenses);
            $albumItem->total_income($albumTotalIncome);
            $albumItem->total_expenses($albumTotalExpenses);
            $albumItem->previous_balance($albumPreviousBalance);
            $albumItem->total($albumTotal);
            $albumItem->license_income_subtotal($albumLicenseIncomeSubtotal);

            my $crossFlag;
            my $crossMapping = RPS::DB::Item::ArtistContractCrossedMap->Lookup(album_id => $albumID, artist_contract_id => $contractID, run_id => $self->_runID());
            if ($crossMapping)
            {
                $crossFlag = 1;
            }

            $albumItem->is_cross_collateralized($crossFlag);

            # !!! Don't save this if the album id is 0.
            # !!!
            if ($albumID > 0)
            {
                $albumItem->save();
            }


            # contract-level license income gets lumped in with the 'crossed' album data.
            #
            if ($crossFlag || $albumID == 0)
            {
                $statementLicenseIncomeSubtotal += $albumLicenseIncomeSubtotal;
            }


            if ($crossFlag)
            {
                $crossIncome += $albumTotalIncome;
                $crossExpense += $albumTotalExpenses;
                $crossBalance += $albumPreviousBalance;
                $crossTotal += $albumTotal;

            }
            elsif ($albumID > 0)
            {
                $unCrossedTotal += $albumTotal;

                if ($albumTotal > 0)
                {
                    $statementSubtotal += $albumTotal;
                }
            }
            if (0 == $albumID)
            {
                $crossTotal += $albumLicenseIncomeSubtotal;
            }
        }
    }


    # Are we carrying a balance for the contract level license income?
    # We need to account for that in the crossed total.
    #
    my $contractLevelLicenseIncomePreviousBalance = RPS::DB::Item::ContractLevelLicenseIncomeBalanceAccount->GetCurrentBalance($artistPayeeID, $payorID);
    my $contractLevelLicenseIncomeTotal = $contractLevelLicenseIncomeSubtotal + $contractLevelLicenseIncomePreviousBalance;
    $crossTotal += $contractLevelLicenseIncomePreviousBalance;
    $crossBalance += $contractLevelLicenseIncomePreviousBalance;

#        $crossTotal += $contractLevelLicenseIncomeSubtotal;
    if ($crossTotal > 0)
    {
        $statementSubtotal += $crossTotal;
    }


    # !!! Deal with the pending balances here.
    my $transactionSubtotal;
    if ($payeePendingTransactions)
    {
        while (my $pendingTrans = $payeePendingTransactions->next())
        {
            $transactionSubtotal += $pendingTrans->amount;
            my $statementTransItem = RPS::DB::Item::ArtistRoyaltyTransaction->Create
            (
             'artist_royalty_statement_id'  => $statementID,
             'amount'                       => $pendingTrans->amount,
             'memo'                         => $pendingTrans->memo,
             'check_number'                 => $pendingTrans->check_number,
             'pending_transaction_id'       => $pendingTrans->pending_transaction_id,
             'transaction_date'             => $pendingTrans->transaction_date,
             'type_code'                    => $pendingTrans->type_code,
            );
            $statementTransItem->save();
        }
    }

    my $payeeBalance = $payeePreviousBalance + $statementSubtotal + $transactionSubtotal;


    # see if the minimum payment threshold has been breached.
    #
    my $amountDue = 0;
    if ($payeeBalance >= $minPayment)
    {
        $amountDue = $payeeBalance;
    }


    # If this artist_payee is 'on_hold', then we tag the statement accordingly, and
    # set amount_due to 0.
    #
    my $onHold = 0;
    if (RPS::DB::Item::ArtistPayee::kStatusOnHold == $artistPayeeData->status)
    {
        $onHold = 1;
        $amountDue = 0;
    }

    # Save the grand total
    #
    $statement->previous_balance($payeePreviousBalance);
    $statement->min_payment($minPayment);
    $statement->balance($payeeBalance);
    $statement->total($statementSubtotal);
    $statement->amount_due($amountDue);
    $statement->on_hold($onHold);
    $statement->transaction_subtotal($transactionSubtotal);

    $statement->cross_collateralized_income_subtotal($crossIncome);
    $statement->cross_collateralized_expense_subtotal($crossExpense);
    $statement->cross_collateralized_previous_balance($crossBalance);
    $statement->cross_collateralized_subtotal($crossTotal);
    $statement->uncrossed_subtotal($unCrossedTotal);

    $statement->license_income_subtotal($statementLicenseIncomeSubtotal);
    $statement->contract_level_license_income_subtotal($contractLevelLicenseIncomeSubtotal);
    $statement->contract_level_license_income_total($contractLevelLicenseIncomeTotal);
    $statement->contract_level_license_income_previous_balance($contractLevelLicenseIncomePreviousBalance);

    $statement->save();
}


sub _createUnitReserve
{
    my ($self, $incomeItem, $productID) = @_;

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

    my $term = RPS::DB::Item::NewArtistContractTerm->Lookup(artist_contract_term_id => $incomeItem->artist_contract_term_id);

    my $units = $incomeItem->units_reserved;

    my %scheduleTable;
    my $schedule = RPS::DB::Item::ReserveLiquidation->GetByIDType($term->artist_contract_id, RPS::DB::Item::ReserveLiquidation::kTypeArtistContract);

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

    my $albumID = $incomeItem->album_id;
    my $trackID = $incomeItem->track_id;
    $trackID = 0 unless $trackID;

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

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

        my $newReserve = RPS::DB::Item::ArtistContractTermReserve->Create
        (
            artist_contract_term_id     => $incomeItem->artist_contract_term_id,
            original_statement_item_id  => $incomeItem->artist_royalty_income_item_id,
            income_source_id            => $incomeItem->income_source_id,
            region_id                   => $incomeItem->region_id,
            channel_id                  => $incomeItem->channel_id,
            price_level_id              => $incomeItem->price_level_id,
            product_id                  => $productID,
            units                       => $periodUnits,
            revenue_based               => 0,
            effective_rate              => $incomeItem->rate,  # This column should be called 'base_rate'...
            #net_rate              			=> $incomeItem->net_rate,  # This column should be called 'effective_rate'...
            price                       => $incomeItem->price,
            periods_remaining           => $i + 1,
            album_id                    => $albumID,
            track_id                    => $trackID,
        );

        $newReserve->save();
        $createdReserve = 1;

        my $runMapItem = RPS::DB::Item::ArtistContractTermReserveRun->Create
        (
            artist_contract_term_reserve_id => $newReserve->artist_contract_term_reserve_id,
            artist_royalty_run_id => $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::ArtistRoyalty::Process::Exception->new("reserve was not created successfully for income item: ", $incomeItem);
    }

}

sub _createRevenueReserve
{
    my ($self, $incomeItem, $productID) = @_;

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

    my $term = RPS::DB::Item::NewArtistContractTerm->Lookup(artist_contract_term_id => $incomeItem->artist_contract_term_id);
    my $revenue = $incomeItem->revenue_reserved;

    my %scheduleTable;
    my $schedule = RPS::DB::Item::ReserveLiquidation->GetByIDType($term->artist_contract_id, RPS::DB::Item::ReserveLiquidation::kTypeArtistContract);

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

    my $albumID = $incomeItem->album_id;
    my $trackID = $incomeItem->track_id;
    $trackID = 0 unless $trackID;

    my $createdReserve = 0;
    my $initialRevenue = $revenue;
    for (my $i = 1; $revenue > 0 && $i <= 8; $i++)
    {
        my $p = $scheduleTable{$i};
        next unless $p;

        my $periodRevenue = Common::RSMath::round($initialRevenue * ($p / 100), 2);
        $periodRevenue = $revenue unless ($periodRevenue < $revenue && $numPeriods > 1);
        $revenue -= $periodRevenue;
        $numPeriods--;


        my $newReserve = RPS::DB::Item::ArtistContractTermReserve->Create
        (
            artist_contract_term_id     => $incomeItem->artist_contract_term_id,
            original_statement_item_id  => $incomeItem->artist_royalty_income_item_id,
            income_source_id            => $incomeItem->income_source_id,
            region_id                   => $incomeItem->region_id,
            channel_id                  => $incomeItem->channel_id,
            price_level_id              => $incomeItem->price_level_id,
            product_id                  => $productID,
            revenue                     => $periodRevenue,
            revenue_based               => 1,
            effective_rate              => $incomeItem->rate,  # This column should be called 'base_rate'...
            #net_rate              			=> $incomeItem->net_rate,  # This column should be called 'effective_rate'...
            price                       => $incomeItem->price,
            periods_remaining           => $i+1,
            album_id                    => $albumID,
            track_id                    => $trackID,
        );

        $newReserve->save();
        $createdReserve = 1;

        my $runMapItem = RPS::DB::Item::ArtistContractTermReserveRun->Create
        (
            artist_contract_term_reserve_id => $newReserve->artist_contract_term_reserve_id,
            artist_royalty_run_id => $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::ArtistRoyalty::Process::Exception->new("reserve was not created successfully for income item: ", $incomeItem);
    }
}


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

    my ($albumID, $trackID);

    if (RPS::DB::Item::Product::kProductTypeDigitalTrack == $product->product_type_id)
    {
        $trackID = $product->asset_id;
        my $trackData = RPS::DB::Item::Track->Lookup(track_id => $trackID);
        $albumID = $trackData->album_id;
    }
    else
    {
        $albumID = $product->asset_id;
    }

    return ($albumID, $trackID);
}


sub _determineChannelID
{
    my ($self, $serviceID, $formatType) = @_;

    # First, try and do an 'exact' match.
    # If that doesn't work, then we'll try using the 'wildcard' service.
    #
    my $channelID = RPS::DB::Item::ServiceFormatChannelMap->GetChannelIDByServiceAndFormat($serviceID, $formatType);
    if (! $channelID)
    {
        $channelID = RPS::DB::Item::ServiceFormatChannelMap->GetChannelIDByServiceAndFormat(RPS::DB::Item::ServiceFormatChannelMap::kAnyService, $formatType);
    }

    return $channelID;
}


sub _determineRegionID
{
    my ($self, $countryCode) = @_;

    my $region = RPS::DB::Item::RegionCountryMap->GetRegionByCountryCode($countryCode);

    if (! $region)
    {
        return RPS::DB::Item::Region::kGlobal;
    }

    return $region->region_id;
}


sub _determineIncomeSourceID
{
    my ($self, $productType, $formatType) = @_;
    my $incomeSourceID;
    $formatType = uc($formatType);
    $productType = uc($productType);

    # Video tracks will be treated like digital tracks.
    #
    if ('V' eq $formatType)
    {
        if ('T' eq $productType)
        {
            $incomeSourceID = RPS::DB::Item::IncomeSource::kIncomeSourceDigitalTrack;
        }
        
    }
    elsif ('D' eq $formatType)
    {
        if ('T' eq $productType)
        {
            $incomeSourceID = RPS::DB::Item::IncomeSource::kIncomeSourceDigitalTrack;
        }
        elsif ('A' eq $productType)
        {
            $incomeSourceID = RPS::DB::Item::IncomeSource::kIncomeSourceDigitalAlbum;
        }

    }
    elsif ('H' eq $formatType)
    {
        if ('T' eq $productType)
        {
            $incomeSourceID = RPS::DB::Item::IncomeSource::kIncomeSourceDigitalTrackPremium;
        }
        elsif ('A' eq $productType)
        {
            $incomeSourceID = RPS::DB::Item::IncomeSource::kIncomeSourceDigitalAlbumPremium;
        }

    }
    elsif ('I' eq $formatType)
    {
        if ('T' eq $productType)
        {
            $incomeSourceID = RPS::DB::Item::IncomeSource::kIncomeSourceDigitalTrackUpgrade;
        }
        elsif ('A' eq $productType)
        {
            $incomeSourceID = RPS::DB::Item::IncomeSource::kIncomeSourceDigitalAlbumUpgrade;
        }

    }
    elsif ('S' eq $formatType)
    {
        $incomeSourceID = RPS::DB::Item::IncomeSource::kIncomeSourceDigitalStream;
    }
    elsif ('T' eq $formatType)
    {
        $incomeSourceID = RPS::DB::Item::IncomeSource::kIncomeSourceDigitalTethered;
    }
    elsif ('R' eq $formatType)
    {
        $incomeSourceID = RPS::DB::Item::IncomeSource::kIncomeSourceRingtone;
    }
    elsif ('B' eq $formatType)
    {
        $incomeSourceID = RPS::DB::Item::IncomeSource::kIncomeSourceBackground;
    }
    elsif ('P' eq $formatType)
    {
        $incomeSourceID = RPS::DB::Item::IncomeSource::kIncomeSourceVPD;
    }
    elsif ('E' eq $formatType)
    {
        $incomeSourceID = RPS::DB::Item::IncomeSource::kIncomeSourceDualDownload;
    }
    elsif ('J' eq $formatType)
    {
        $incomeSourceID = RPS::DB::Item::IncomeSource::kIncomeSourceJukebox;
    }
    else
    {
        if ('2' eq $productType)
        {
            $incomeSourceID = RPS::DB::Item::IncomeSource::kIncomeSourceCD;
        }
        elsif ('1' eq $productType)
        {
            $incomeSourceID = RPS::DB::Item::IncomeSource::kIncomeSourceLP;
        }
        elsif ('P' eq $productType)
        {
            $incomeSourceID = RPS::DB::Item::IncomeSource::kIncomeSourceLP5;
        }
        elsif ('4' eq $productType)
        {
            $incomeSourceID = RPS::DB::Item::IncomeSource::kIncomeSourceCassette;
        }
        elsif ('M' eq $productType)
        {
            $incomeSourceID = RPS::DB::Item::IncomeSource::kIncomeSourceMasterLicense;
        }
        elsif ('S' eq $productType)
        {
            $incomeSourceID = RPS::DB::Item::IncomeSource::kIncomeSourceSyncLicense;
        }
        elsif ('9' eq $productType)
        {
            $incomeSourceID = RPS::DB::Item::IncomeSource::kIncomeSourceDVD;
        }
        elsif ('U' eq $productType)
        {
            $incomeSourceID = RPS::DB::Item::IncomeSource::kIncomeSourceBluRay;
        }
        elsif ('D' eq $productType)
        {
            $incomeSourceID = RPS::DB::Item::IncomeSource::kIncomeSourceDVDCDSet;
        }
        elsif ('3' eq $productType)
        {
            $incomeSourceID = RPS::DB::Item::IncomeSource::kIncomeSourceVHS;
        }
        elsif ('B' eq $productType)
        {
            $incomeSourceID = RPS::DB::Item::IncomeSource::kIncomeSourceDblCD;
        }
        elsif ('C' eq $productType)
        {
            $incomeSourceID = RPS::DB::Item::IncomeSource::kIncomeSourceCDSingle;
        }
        elsif ('K' eq $productType)
        {
            $incomeSourceID = RPS::DB::Item::IncomeSource::kIncomeSourceCassette5;
        }
        else
        {
            warn "ERROR - unable to determine income source id from productType $productType formatType $formatType";
        }

    }

    # !!! Add support for VPD when we have choosen codes.

    return $incomeSourceID;
}


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

    return
        $productTypeID == RPS::DB::Item::Product::kProductTypeDigital       ? RPS::File::Sale::TYPE_ALBUM :
        $productTypeID == RPS::DB::Item::Product::kProductTypeDigitalTrack  ? RPS::File::Sale::TYPE_TRACK :
        $productTypeID == RPS::DB::Item::Product::kProductTypeLP            ? RPS::File::Sale::TYPE_LP :
        $productTypeID == RPS::DB::Item::Product::kProductTypeLP5           ? RPS::File::Sale::TYPE_LP5 :
        $productTypeID == RPS::DB::Item::Product::kProductTypeCD            ? RPS::File::Sale::TYPE_CD :
        $productTypeID == RPS::DB::Item::Product::kProductTypeVHS           ? RPS::File::Sale::TYPE_VHS :
        $productTypeID == RPS::DB::Item::Product::kProductTypeCass          ? RPS::File::Sale::TYPE_CASS :
        $productTypeID == RPS::DB::Item::Product::kProductTypeEP            ? RPS::File::Sale::TYPE_EP :
        $productTypeID == RPS::DB::Item::Product::kProductTypeDVD           ? RPS::File::Sale::TYPE_DVD :
        $productTypeID == RPS::DB::Item::Product::kProductTypeBluRay        ? RPS::File::Sale::TYPE_BLURAY :
        $productTypeID == RPS::DB::Item::Product::kProductTypeCassSingle    ? RPS::DB::Item::Product::kProductTypeCassSingle :
        $productTypeID == RPS::DB::Item::Product::kProductTypeCDSingle      ? RPS::DB::Item::Product::kProductTypeCDSingle :
        $productTypeID == RPS::DB::Item::Product::kProductTypeDVDCDSet      ? RPS::File::Sale::TYPE_DVD_CD_SET :
        $productTypeID == RPS::DB::Item::Product::kProductTypeDblCD         ? RPS::File::Sale::TYPE_DBL_CD :
        undef;
}


sub getAllPayorIDs
{

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

    return \@ids;
}


sub _subtractStatRateFromNetRevenue
{
    my $clientInfo = Common::DB::Item::Client->Lookup(client_id => Common::RSApp::GetClientID());
    return $clientInfo->subtract_stat_rate_from_net_revenue;
}


my $productTypeTable;
sub _canTakeReserves
{
    my ($productID) = @_;

    return 0 unless $productID;


    if (! $productTypeTable)
    {
        $productTypeTable = {};
        my $productTypes = RPS::DB::Item::ProductType->GetAll();
        while (my $pt = $productTypes->next())
        {
            $productTypeTable->{$pt->product_type_id} = $pt->type;
        }
    }

    my $product = RPS::DB::Item::Product->Lookup(product_id => $productID);

    if (RPS::DB::Item::ProductType::kTypePhysical == $productTypeTable->{$product->product_type_id} )
    {
        return 1;
    }
    return 0;
}


sub _getRetailPrice
{
    my ($self, $priceLevelID, $productID) = @_;
    my ($price, $actualPriceLevelID) = $self->_getPrice($priceLevelID, $productID);

    return ($price->retail, $actualPriceLevelID) if $price;
    return (undef, undef);
}

sub _getWholesalePrice
{
    my ($self,$priceLevelID, $productID) = @_;
    my ($price, $actualPriceLevelID) = $self->_getPrice($priceLevelID, $productID);

    return ($price->wholesale, $actualPriceLevelID) if $price;
    return (undef, undef);
}

sub _getPPDPrice
{
    my ($self, $priceLevelID, $productID) = @_;
    my ($price, $actualPriceLevelID) = $self->_getPrice($priceLevelID, $productID);

    return ($price->ppd, $actualPriceLevelID) if $price;
    return (undef, undef);
}



sub _getPrice
{
    my ($self, $priceLevelID, $productID) = @_;


    #if (! $priceLevelID)
    #{
    #    warn "\nError - no price_level_id passed in!\n";
    #    return undef;
    #}

    if (! $priceLevelID)
    {
        warn "\nError - no price_level_id passed in!\n";
        return undef;
    }

    # First, fetch the product.
    # We need to see if this is a track product... if it is, we'll need
    # to get the album product id.
    # This is because we don't have product_price entries for track products...
    #
    my $product = RPS::DB::Item::Product->Lookup(product_id => $productID);
    if (RPS::DB::Item::Product::kProductTypeDigitalTrack == $product->product_type_id)
    {
        my $albumID;
        my $trackID = $product->asset_id;

        # Fetch this from the track's data.
        #
        my $trackData = RPS::DB::Item::Track->Lookup(track_id => $trackID);
        assert($trackData);
        $albumID = $trackData->album_id;

        # Find the digital album product id
        #
        my $digitalAlbumProductList = RPS::DB::Item::Product->GetProductsByAlbumID($albumID, RPS::DB::Item::Product::kProductTypeDigital);
        if ($digitalAlbumProductList)
        {
            $product = $digitalAlbumProductList->next();
            $productID = $product->product_id;
        }
    }

    if (0 == $priceLevelID)
    {
        $priceLevelID = $product->default_price_level_id;
    }


    my $productPrice = RPS::DB::Item::ProductPrice->Lookup(product_id => $productID, price_level_id => $priceLevelID);
    if (! $productPrice)
    {
        warn "\nError - can't fetch ProductPrice product_id $productID  price_level_id $priceLevelID\n";
        return undef;
    }
    my $price = RPS::DB::Item::Price->Lookup(price_id => $productPrice->price_id);

    return ($price, $priceLevelID);
}


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

    my %contractHash;

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

    my $mapItems = RPS::DB::Item::SaleRunMap->GetPaidArtistRoyaltyBySaleID($saleID);

    $self->_report("found " . $mapItems->size() . " map items for sale $saleID", 4);

    while (my $mapItem = $mapItems->next())
    {
        my $trackID = 0;
        my $contractID = 0;

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

        my $originalIncomeItem = RPS::DB::Item::ArtistRoyaltyIncomeItem->Lookup(artist_royalty_income_item_id => $mapItem->statement_item_id);
        if (! $originalIncomeItem)
        {
            $self->_report("ERROR - cannot find original income item for sale map item: " . Dumper($mapItem));
            next;
        }
        #my $term = RPS::DB::Item::NewArtistContractTerm->Lookup(artist_contract_term_id => $originalIncomeItem-#>artist_contract_term_id);
        #if (! $term)
        #{
        #    $self->_report("ERROR - cannot find associated term for sale map item: " . Dumper($mapItem));
        #    next;
        #}

        my $royaltyAlbum = RPS::DB::Item::ArtistRoyaltyAlbum->Lookup(artist_royalty_album_id => $originalIncomeItem->artist_royalty_album_id);
        if (! $royaltyAlbum)
        {
            $self->_report("ERROR - cannot find associated royalty album for sale map item: " . Dumper($mapItem));
            next;
        }

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

        $self->_report("found paid contract $contractID", 4);

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

    return \%contractHash;
}


sub _isAlbumIncomeSource
{
    my ($self, $incomeSourceID) = @_;
    if (RPS::DB::Item::IncomeSource::kIncomeSourceDigitalAlbum == $incomeSourceID
     || RPS::DB::Item::IncomeSource::kIncomeSourceCD == $incomeSourceID
     || RPS::DB::Item::IncomeSource::kIncomeSourceLP == $incomeSourceID
     || RPS::DB::Item::IncomeSource::kIncomeSourceCassette == $incomeSourceID
     || RPS::DB::Item::IncomeSource::kIncomeSourceCDSingle == $incomeSourceID
     || RPS::DB::Item::IncomeSource::kIncomeSourceDblCD == $incomeSourceID
     || RPS::DB::Item::IncomeSource::kIncomeSourceDigitalAlbumPremium == $incomeSourceID
     || RPS::DB::Item::IncomeSource::kIncomeSourceDigitalAlbumUpgrade == $incomeSourceID
     || RPS::DB::Item::IncomeSource::kIncomeSourceDVD == $incomeSourceID
     || RPS::DB::Item::IncomeSource::kIncomeSourceBluRay == $incomeSourceID
     || RPS::DB::Item::IncomeSource::kIncomeSourceDVDCDSet == $incomeSourceID)
    {
        return 1;
    }

    return 0;
}

sub _isDownload
{
    my ($self, $incomeSourceID) = @_;
    if (RPS::DB::Item::IncomeSource::kIncomeSourceDigitalAlbum == $incomeSourceID
     || RPS::DB::Item::IncomeSource::kIncomeSourceDigitalTrack == $incomeSourceID
     || RPS::DB::Item::IncomeSource::kIncomeSourceDigitalAlbumPremium == $incomeSourceID
     || RPS::DB::Item::IncomeSource::kIncomeSourceDigitalTrackPremium == $incomeSourceID
     || RPS::DB::Item::IncomeSource::kIncomeSourceDigitalAlbumPremium == $incomeSourceID
     || RPS::DB::Item::IncomeSource::kIncomeSourceDigitalTrackPremium == $incomeSourceID
    )
    {
        return 1;
    }

    return 0;
}

my %gFileDistFeeMap;
sub _getDistributionFeeFromSale
{
    my ($self, $sale) = @_;

    if (! defined $gFileDistFeeMap{$sale->file_id})
    {
        $gFileDistFeeMap{$sale->file_id} = RPS::DB::Item::DistributionFee->GetDistFeeBySale(file_id => $sale->file_id, sale_id => $sale->sale_id);
    }
    return $gFileDistFeeMap{$sale->file_id};
}


sub _createStatementData
{
    my ($self, $sales, $returns, $rate, $totalRevenue, $averagePrice, $term, $albumID, $trackID, $productID, $incomeSourceID, $regionID, $channelID, $priceLevelID, $contract, $rateTypeID, $price, $saleID) = @_;

    my $payorID = $contract->payor_id;
    assert($payorID);

    # Round the rate, to make sure we are always hashing on the same things
    # (i.e. 40 vs 40.0000)
    #
    $rate = Common::RSMath::round($rate, 4);

    my $termRateTypeID = $term->contract_rate_type_id;
    my $termID = $term->artist_contract_term_id;

    # If we are using the document price level, we need to grab the relevant price
    # from the sale record.
    #
    if ($rateTypeID == RPS::DB::Item::ContractRateType::kRateTypePercentDocumentRetail
        || $rateTypeID == RPS::DB::Item::ContractRateType::kRateTypePercentDocumentWholesale)
    {
        ($price) = $self->_getDocumentPrice($rateTypeID, $saleID);
    }
    # !!! Putting this in here to try and get master use sales to work...
    elsif ($productID)
    {
        ($price, $priceLevelID) = $self->_determinePrice($rateTypeID, $averagePrice, $priceLevelID, $productID);
    }

    # !!! price and rate will need to be rounded consistently to hash properly
    #
    $price = Common::RSMath::round($price, 4);
    $rate = Common::RSMath::round($rate, 4);


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

    $self->_report("_createStatementData: "
     . $contract->artist_contract_id . ", "
     . "$albumID, "
     . "$trackID, "
     . "$productID, "
     . "$incomeSourceID, "
     . "$regionID, "
     . "$channelID, "
     . "$priceLevelID, "
     . $term->artist_contract_term_id . ", "
     . " rateTypeID=$rateTypeID "
     . "$price, $rate : sales = $sales returns = $returns revenue = $totalRevenue", 4);

    # Which of these things do I expect to always have?
    #
    assert(defined $incomeSourceID);

    $gData
    {$contract->artist_contract_id}
    {$albumID}
    {$trackID}
    {$productID}
    {incomeSourceID}
    {$incomeSourceID}
    {$regionID}
    {$channelID}
    {$priceLevelID}
    {$term->artist_contract_term_id}
    {$price}
    {$rate}
    {lineitem}{sales} += $sales;

    $gData
    {$contract->artist_contract_id}
    {$albumID}
    {$trackID}
    {$productID}
    {incomeSourceID}
    {$incomeSourceID}
    {$regionID}
    {$channelID}
    {$priceLevelID}
    {$term->artist_contract_term_id}
    {$price}
    {$rate}
    {lineitem}{returns} += $returns;

    $gData
    {$contract->artist_contract_id}
    {$albumID}
    {$trackID}
    {$productID}
    {incomeSourceID}
    {$incomeSourceID}
    {$regionID}
    {$channelID}
    {$priceLevelID}
    {$term->artist_contract_term_id}
    {$price}
    {$rate}
    {lineitem}{revenue} += $totalRevenue;


    if ($saleID)
    {
        $gData
        {$contract->artist_contract_id}
        {$albumID}
        {$trackID}
        {$productID}
        {incomeSourceID}
        {$incomeSourceID}
        {$regionID}
        {$channelID}
        {$priceLevelID}
        {$term->artist_contract_term_id}
        {$price}
        {$rate}
        {lineitem}{saleIDs}{$saleID} = 1;
    }
}


sub _getAlbumPreviousBalance
{
    my ($self, $payorID, $artistPayeeID, $albumID, $artistContractID) = @_;

    my $balance = 0;
    my $accountMap = RPS::DB::Item::ArtistRoyaltyAlbumBalanceAccount->Lookup
    (
        album_id => $albumID,
        artist_payee_id => $artistPayeeID,
        payor_id => $payorID,
        artist_contract_id => $artistContractID,
    );
    if ($accountMap)
    {
        my $accountID = $accountMap->account_id;
        $balance = RPS::DB::Item::FinanceAccount->CurrentBalance($accountID);
    }

    return $balance;
}


sub _getNumAlbumTracks
{
    my ($self, $albumID) = @_;

    my $allTracks = RPS::DB::Item::Track->GetTracksByAlbumID($albumID);
    my $numTracks = $allTracks->size();

    return $numTracks;
}


sub _determinePrice
{
    my ($self, $rateType, $averagePrice, $priceLevelID, $productID) = @_;

    my ($price, $actualPriceLevelID);

    if (RPS::DB::Item::ContractRateType::kRateTypePercentRevenue == $rateType)
    {
        # price is meaningless for net revenue rates.
        # So... do nothing.
    }
    elsif(RPS::DB::Item::ContractRateType::kRateTypePercentRetail == $rateType)
    {
        ($price, $actualPriceLevelID) = $self->_getRetailPrice($priceLevelID, $productID);
    }
    elsif (RPS::DB::Item::ContractRateType::kRateTypePercentWholesale == $rateType)
    {
        ($price, $actualPriceLevelID) = $self->_getWholesalePrice($priceLevelID, $productID);
    }
    elsif (RPS::DB::Item::ContractRateType::kRateTypePercentPPD == $rateType)
    {
        ($price, $actualPriceLevelID) = $self->_getPPDPrice($priceLevelID, $productID);
    }
    elsif (RPS::DB::Item::ContractRateType::kRateTypePercentAverage == $rateType)
    {
        $price = $averagePrice;
    }
    elsif (RPS::DB::Item::ContractRateType::kRateTypeFixed == $rateType)
    {
        # This is a little different.
        # The price is stored in the rate column.
        # In other words, the base rate _is_ the price.
        # We'll leave it there, so it shows up on the statement
        # in the 'rate' column.
        #
        $price = 1;

        # I think the base rate, though, is not a percentage, but a real rate.
        # This means we want to _not_ divide the rate by 100 during our
        # normal calculations.
    }

    return ($price, $actualPriceLevelID);
}


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

    my $price;

    my $sale = Raptor::DB::Item::Sale->Lookup(sale_id => $saleID);

    my $conversionRate = $sale->conversion_rate;

    assert( $conversionRate, "Conversion rate must be > 0" );

    if ($rateTypeID == RPS::DB::Item::ContractRateType::kRateTypePercentDocumentRetail)
    {
        $price = $sale->retail_price * $conversionRate;
    }
    elsif ($rateTypeID == RPS::DB::Item::ContractRateType::kRateTypePercentDocumentWholesale)
    {
        $price = $sale->wholesale_price * $conversionRate;
    }

    return $price;

}

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

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

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

# I need to be able to call this from the run controller.
#
sub logMissedSale
{
    my ($runID, $sale, $why, %args) = @_;

    # Get as much state as we can out of the sale.  Some of this information
    # may be missing (which could explain why the sale was skipped...)
    #
    my $newLogEntry = RPS::DB::Item::ArtistRoyaltyRunMissedSaleLog->Create();

    $newLogEntry->artist_royalty_run_id($runID);
    $newLogEntry->sale_id($sale->sale_id);
    $newLogEntry->reason($why);

    my $product;
    if ($args{product})
    {
        $product = $args{product};
    }
    elsif ($args{productID})
    {
        $product = RPS::DB::Item::Product->Lookup(product_id => $args{productID});
    }
    if ($product)
    {
        $newLogEntry->product_id($product->product_id());
        $newLogEntry->product_type_id($product->product_type_id());
        $newLogEntry->product_type(_idToProductType($product->product_type_id()));

        # !!! If we have a product, but no track or album id, let's fetch that stuff.
        #
        if (! $args{albumID} || ! $args{trackID})
        {
            my ($albumID, $trackID) = getAlbumTrackFromProduct($product);
            $args{albumID} = $albumID;
            $args{trackID} = $trackID;
        }
    }


    my $artistID;
    my $album;
    if ($args{album})
    {
        $album = $args{album}
    }
    elsif ($args{albumID})
    {
        $album = RPS::DB::Item::Album->Lookup(album_id => $args{albumID});
    }
    if ($album)
    {
        $newLogEntry->album_id($album->album_id());
        $newLogEntry->album_title($album->title());
        $newLogEntry->catalog_number($album->catalog_number());

        $artistID = $album->artist_id;
    }


    my $track;
    if ($args{track})
    {
        $track = $args{track};
    }
    elsif ($args{trackID})
    {
        $track = RPS::DB::Item::Track->Lookup(track_id => $args{trackID});
    }
    if ($track)
    {
        $newLogEntry->track_id($track->track_id());
        $newLogEntry->track_title($track->title());
        $artistID = $track->artist_id();
    }

    my $contractID;
    if ($args{contract})
    {
        $contractID = $args{contract}->artist_contract_id;
        $newLogEntry->artist_contract_id($contractID);
        $newLogEntry->artist_contract_title($args{contract}->title);
    }

    if ($args{details})
    {
        $newLogEntry->details($args{details});
    }

    if ($artistID)
    {
        my $artist = RPS::DB::Item::Artist->Lookup(artist_id => $artistID);
        if ($artist)
        {
            $newLogEntry->artist_name($artist->name);
        }
    }

    # !!! Why do we really need to show this crap?
    #
    my $units = $sale->units();
    if (! $units)
    {
        $units = $sale->sales() - $sale->returns();
    }
    $newLogEntry->units($units);
    $newLogEntry->total_revenue($sale->total_revenue());

    # If we already have an entry for this sale/contract combination, don't bother making another one.
    #
    my $saveEntry = 1;

    if ($args{contract})
    {
        my $dupeCheck = RPS::DB::Item::ArtistRoyaltyRunMissedSaleLog->Lookup(sale_id => $sale->sale_id, artist_contract_id => $contractID, artist_royalty_run_id => $runID);
        if ($dupeCheck)
        {
            $saveEntry = 0;
        }
    }

    if ($saveEntry == 1)
    {
        $newLogEntry->save();
    }
}


sub _getAlbumBalances
{
    my ($self) = @_;
    my %resultHash;

    my $accountMap = RPS::DB::Item::ArtistRoyaltyAlbumBalanceAccount->GetByPayee($self->_payeeID());
    while (my $mapping = $accountMap->next())
    {
        # I only need to concern myself with non-0 balances, right?
        #
        my $currentBalance = RPS::DB::Item::FinanceAccount->CurrentBalance($mapping->account_id);
        if ($currentBalance != 0)
        {
            $resultHash{$mapping->album_id}{$mapping->artist_contract_id} = $currentBalance;
        }
    }
    $self->_report("_getAlbumBalances: ", 4);
    $self->_report(\%resultHash, 4);

    return \%resultHash;
}





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

    if (! $gChannelIDMap)
    {
        $gChannelIDMap = {};

        my $allChannels = RPS::DB::Item::Channel->GetAll();
        while (my $is = $allChannels->next())
        {
            $gChannelIDMap->{$is->channel_id} = $is->name;
        }
    }
    my $channelName = '(Missing)';

    if ($gChannelIDMap->{$id})
    {
        $channelName = $gChannelIDMap->{$id};
    }

    return $channelName;
}


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

    if (! $gIncomeSourceIDMap)
    {
        $gIncomeSourceIDMap = {};

        my $allIncomeSources = RPS::DB::Item::IncomeSource->GetAll();
        while (my $is = $allIncomeSources->next())
        {
            $gIncomeSourceIDMap->{$is->income_source_id} = $is->description;
        }
    }

    my $incomeSourceName = '(Missing)';

    if ($gIncomeSourceIDMap->{$id})
    {
        $incomeSourceName = $gIncomeSourceIDMap->{$id};
    }

    return $incomeSourceName;
}


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

    if (! $gPriceLevelIDMap)
    {
        $gPriceLevelIDMap= {};

        my $all = RPS::DB::Item::PriceLevel->GetAll();
        while (my $pl = $all->next())
        {
            $gPriceLevelIDMap->{$pl->price_level_id} = $pl->name;
        }
    }

    my $priceLevelName = '(Missing)';

    if ($gPriceLevelIDMap->{$id})
    {
        $priceLevelName = $gPriceLevelIDMap->{$id};
    }

    return $priceLevelName;
}


my $gProductTypeIDMap;
sub _idToProductType
{
    my ($id) = @_;

    if (! $gProductTypeIDMap)
    {
        $gProductTypeIDMap = {};

        my $all = RPS::DB::Item::ProductType->GetAll();
        while (my $pt = $all->next())
        {
            $gProductTypeIDMap->{$pt->product_type_id} = $pt->description;
        }
    }

    my $productTypeName = '(Missing)';

    if ($gProductTypeIDMap->{$id})
    {
        $productTypeName = $gProductTypeIDMap->{$id};
    }

    return $productTypeName;
}


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

    if (! $gSaleProductTypeIDMap)
    {
        $gSaleProductTypeIDMap = {};

        my $all = RPS::DB::Item::SaleProductType->GetAll();
        while (my $pt = $all->next())
        {
            $gSaleProductTypeIDMap->{$pt->sale_product_type_id} = $pt->description;
        }
    }

    my $saleProductTypeName = '(Missing)';

    if ($gSaleProductTypeIDMap->{$id})
    {
        $saleProductTypeName = $gSaleProductTypeIDMap->{$id};
    }

    return $saleProductTypeName;
}


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

    if (! $gFormatTypeIDMap)
    {
        $gFormatTypeIDMap = {};

        my $all = RPS::DB::Item::Format->GetAll();
        while (my $ft = $all->next())
        {
            $gFormatTypeIDMap->{$ft->format_type} = $ft->format_name;
        }
    }

    my $formatTypeName = '(Missing)';

    if ($gFormatTypeIDMap->{$id})
    {
        $formatTypeName = $gFormatTypeIDMap->{$id};
    }

    return $formatTypeName;
}



1;
