#---------------------------------------------------------------
# ____                   _ _         ____  _                    
#|  _ \ ___  _   _  __ _| | |_ _   _/ ___|| |__   __ _ _ __ ___ 
#| |_) / _ \| | | |/ _` | | __| | | \___ \| '_ \ / _` | '__/ _ \
#|  _ < (_) | |_| | (_| | | |_| |_| |___) | | | | (_| | | |  __/
#|_| \_\___/ \__, |\__,_|_|\__|\__, |____/|_| |_|\__,_|_|  \___|
#            |___/             |___/                            
#
# Copyright (C) 2012 RoyaltyShare, Inc.   All Rights Reserved
#---------------------------------------------------------------


package RPS::ArtistRoyalty::Fast::Script::MapSales;

use strict;
use Data::Dumper;
use File::Path;
use POSIX ":sys_wait_h";


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

use DB_File;

use Common::Util;
use Common::Log;
use Common::Parser;
use Common::WriteXML;
use Common::RSMath;

use RPS::DB::Item::Label;
use RPS::DB::Item::Artist;
use RPS::DB::Item::Album;
use RPS::DB::Item::Product;
use RPS::DB::Item::Song;
use RPS::DB::Item::Master;
use RPS::DB::Item::Track;
use RPS::DB::Item::ProductTrack;
use RPS::DB::Item::Product;
use RPS::DB::Item::Price;
use RPS::DB::Item::ProductPrice;
use RPS::DB::Item::Channel;
use RPS::DB::Item::PriceLevel;
use RPS::DB::Item::IncomeSource;
use RPS::DB::Item::ContractRateType;
use RPS::DB::Item::SaleProductType;
use RPS::DB::Item::Format;
use RPS::DB::Item::IncomeSource;
use RPS::DB::Item::ContractRateType;

use RPS::ArtistRoyalty::Fast::Static::Sales;
use RPS::ArtistRoyalty::Fast::Static::ProductTerms;
use RPS::ArtistRoyalty::Fast::Static::ProductPrices;
use RPS::ArtistRoyalty::Fast::Static::PaidSales;
use RPS::ArtistRoyalty::Fast::Static::IncomeSources;
use RPS::ArtistRoyalty::Fast::Static::RegionCountries;
use RPS::ArtistRoyalty::Fast::Static::Terms;

use RPS::ArtistRoyalty::Fast::Mapped::Data;
use RPS::ArtistRoyalty::Fast::Mapped::MissedSales;

use RPS::ArtistRoyalty::Fast::TermType;


use base 'Common::Script';


sub _options {{
    client_id => 
    {
        short       => 'c',
        required    => 1,
        description => 'Limit to this client id',
        parameter    => 'i'
    },
    data_path => 
    {
        short       => 'd',
        required    => 1,
        description => 'path to directory containing static data',
        parameter    => 's'
    },
    output_path => 
    {
        short       => 'o',
        required    => 1,
        description => 'path to directory to write output files',
        parameter    => 's'
    },
    process_count =>
    {
        short       => 'p',
        required    => 0,
        description => 'number of processes to use. defaults to 1',
        parameter   => 'i',
    },
    index_range =>
    {
        short       => 'i',
        required    => 0,
        description => 'First,Last data indexes to process.  Ex: -s0,10000',
        parameter   => 's',
    },
}}


my $gChildCount;

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

Log->warn("BEGIN");

    # JPK - I'm considering a different model, where I just fork a couple of child processes
    #       to do the work.
    #
    my ($firstSale, $lastSale) = $self->_parseFirstLastSaleIndex($self->param('index_range'));

    my $dataPath = $self->param('data_path');

    my $outputDirectory = $self->param('output_path');

    # JPK - Should it be the responsibility of this process to create the directory if
    # it doesn't already exist?   It's a bit of a stretch, but then again it's convenient...
    #
    if (! -d $outputDirectory)
    {
        # !!! Might want to make this behavior optional...
        #
        mkpath($outputDirectory);
    }


    # Fetch an array reference to all the sales data.
    #
    # !!! Not sure this makes any sense, really, to pass the indexes to that interface.
    # !!! If we're getting a tied array reference back, it might be better to
    # !!! just constrain the loop.
    #
    # !!! I'm thinking of loading all the static data, then getting the sales,
    # !!! then calculating the offsets, and then forking.
    Log->info("Getting Sales");
    my $sales = RPS::ArtistRoyalty::Fast::Static::Sales->GetSales($dataPath, $firstSale, $lastSale);


    # The term data is stored as a hash of serialized array refs.
    # So we'll need to eval each record.
    # JPK - Not particularly abstract, true, but I don't want to waste time instantiating
    # some sort of wrapper object for each product.
    #
    Log->info("Getting ProductTerms");
    my $productTermData = RPS::ArtistRoyalty::Fast::Static::ProductTerms->GetProductTermData($dataPath);

    Log->info("Getting ProductPrices");
    my $productPriceData = RPS::ArtistRoyalty::Fast::Static::ProductPrices->GetProductPriceData($dataPath);


    # We'll map sale_id to an array of term_id,track_id pairs, using get_dup.
    #
    # It's a little ugly with the get_dup, but there will be a TON of these, and
    # it's not going to be worth it to suffer the overhead of an eval.
    # We get away with that for the productTermData, but that's because the sales
    # are sorted by product_id, so we only have to do that once for each product.
    # And there are fewer products...
    #
    Log->info("Getting PaidSales");
    my $paidSalesData = RPS::ArtistRoyalty::Fast::Static::PaidSales->GetPaidSalesData($dataPath);


    # We'll need all the contract term data.
    # I think this data is small enough to read the whole thing into
    # hash of array refs.
    #
    Log->info("Getting ContractTermData");
    my $contractTermData = RPS::ArtistRoyalty::Fast::Static::Terms->GetContractTermData($dataPath);

    # These maps are also small, so we'll also slurp these up into
    # hashed array refs.
    #
    Log->info("Getting Regions");
    my $regionCountryMap = RPS::ArtistRoyalty::Fast::Static::RegionCountries->GetRegionIDToCountryCodeData($dataPath);

    Log->info("Getting IncomeSources");
    my $sourceIDMap = RPS::ArtistRoyalty::Fast::Static::IncomeSources->GetIncomeSourceIDToContractTermSourceIDData($dataPath);



# JPK - At this point we have handles, file descriptors, etc, from everybody.
#
# !!! If I fork, but then have the parent do work, it might confuse any wrapper scripts, because
# !!! the parent could exit before the children are done.
# !!! So, instead, we'll fork off children to do all the work, and this process will wait until
# !!! they all exit.
#
    my $numSales = scalar (@$sales);
    Log->warn("Number of sales: $numSales");

    my $childCount = $self->param('process_count');
    $childCount = 1 unless $childCount;

    my $salesPerProcess = int($numSales / $childCount);
    Log->warn("sales per process: $salesPerProcess");

    my $firstSale = 0;
    my $lastSale = $firstSale + $salesPerProcess;


    #
    # !!! These are nice to have, I guess, but we'll probably use a simpler global semaphore.
    #
    my @childPIDs;
    $gChildCount = 0;

    for (my $x = 0; $x < $childCount; $x++)
    {
        my $pid = fork();
        if (! $pid)
        {
            # !!! There may be a bit of a race condition here.  I want to spawn all the children, then set up the parent's signal handlers.
            # !!! We might need to wait a bit before proceeding in the child to allow all that to get set up.
            #
            return $self->_doTheMapping($firstSale, $lastSale, $outputDirectory, $sales, $productTermData, $productPriceData, $paidSalesData, $contractTermData, $regionCountryMap, $sourceIDMap);
        }
        else
        {
            $gChildCount++;

            $firstSale = $lastSale;

            # Include all remaining sales if the next child is the last one.
            #
            if ($x == ($childCount - 2))
            {
                $lastSale = $numSales;
            }
            else
            {
                $lastSale = $firstSale + $salesPerProcess;
            }
        }
    }

    $SIG{CHLD} = \&REAPER;
    $SIG{INT} = 'IGNORE';

    while ($gChildCount)
    {
        sleep(2);
    }

    Log->warn("All children finished, exiting");
}

sub CTRL_C
{
    Log->warn("caught a ctrl-c, exiting");
    exit(1);
}

sub REAPER
{
    my $deadPid;
    while (($deadPid = waitpid(-1, &WNOHANG)) > 0)
    {
        $gChildCount--;
    }
    $SIG{CHLD} = \&REAPER;
}

sub _doTheMapping
{
    my ($self, $firstSale, $lastSale, $outputDirectory, $sales, $productTermData, $productPriceData, $paidSalesData, $contractTermData, $regionCountryMap, $sourceIDMap) = @_;

    # This is in the child process.
    # Install a signal handler to catch ctrl-c
    $SIG{INT} = \&CTRL_C;

    my $incomeItemFD = $self->_openIncomeItemFileDescriptor($outputDirectory);
    my $missedSalesFD = $self->_openMissedSalesFileDescriptor($outputDirectory);
    

    my $lastTermsProductID;
    my $saleTermsAndTracks;
    my $saleProductPrices;

    Log->warn("mapping sales $firstSale up to $lastSale");
#    foreach my $saleLine (@$sales)
    for (my $i = $firstSale; $i < $lastSale; $i++)
    {
        my $saleLine = $sales->[$i];

        Log->info("\n\nSALE LINE:", $saleLine);

        # !!! Let's just implement this in-line for now.
        # !!! Save the overhead of function calls, but at the price
        # !!! of a big-ass function and low-abstraction.
        my @saleArray = split("\t", $saleLine);
        my $sale = \@saleArray;

        
        # I think it's worth it to dereference fields for readability.
        #
        my $productID = $sale->[RPS::ArtistRoyalty::Fast::Static::Sales::kProductID];
        my $saleID = $sale->[RPS::ArtistRoyalty::Fast::Static::Sales::kSaleID];


        # Fetch all the term_id,track_id pairs associated with the sale's product_id.
        #
        if ($productID != $lastTermsProductID)
        {
            $saleTermsAndTracks = eval($productTermData->{$productID});
            $saleProductPrices = eval($productPriceData->{$productID});
            $lastTermsProductID = $productID;
        }

        if (! $saleTermsAndTracks
         || ! scalar @$saleTermsAndTracks)
        {
#            print "NOTHING IN  saleTermsAndTracks for product $productID\n";   
            $self->logMissedSale($missedSalesFD, $sale, RPS::ArtistRoyalty::Fast::Mapped::MissedSales::kNoContracts);
            next;
        }


        my %paidMap;
        my @contractTracks = $paidSalesData->get_dup($saleID);
        foreach my $contractTrack (@contractTracks)
        {
            Log->info(" Contract Track for Sale: $saleID", $contractTrack);

            # !!! Look at pack/unpack for this stuff...
            #
            my @bits = split(',', $contractTrack);
            $paidMap{$bits[0]}{$bits[1]} = 1;
        }

        Log->info("paidMap:", \%paidMap);


        # Now we can start digging a bit deeper into the sale, and see which of the remaining terms actually fit it.
        #
        # !!! In old code, we actually ignore the sale.product_type, and instead use a mapping function based
        # !!! on product.product_type_id.
        # !!! It would be swell if we could get that to work in the original query.
        # !!! The problem seems to be that joining the product table, even though product_id is indexed, takes
        # !!! a LOT LONGER, and gets worse as the sale table grows.   So that's not a good solution.
        #
        #
        my $incomeSourceID = $self->_determineIncomeSourceID($sale->[RPS::ArtistRoyalty::Fast::Static::Sales::kProductType], $sale->[RPS::ArtistRoyalty::Fast::Static::Sales::kFormatType]);
        if (! $incomeSourceID)
        {
            $self->logMissedSale($missedSalesFD, $sale, RPS::ArtistRoyalty::Fast::Mapped::MissedSales::kNoIncomeSource);
            next;
        }

        my $saleDate = $sale->[RPS::ArtistRoyalty::Fast::Static::Sales::kDateEnd];
        
        # The terms are already sorted by contract_id and priority.
        #
        my $lastContractID;
        my $lastTrackID;
        my $defaultTerm;
        my $matchingTerm;
        Log->info("saleTermsAndTracks:", $saleTermsAndTracks);

        foreach my $termTrackData (@$saleTermsAndTracks)
        {
            my $contractID = $termTrackData->[RPS::ArtistRoyalty::Fast::Static::ProductTerms::kArtistContractID];
            my $priority = $termTrackData->[RPS::ArtistRoyalty::Fast::Static::ProductTerms::kPriority];
            my $termID = $termTrackData->[RPS::ArtistRoyalty::Fast::Static::ProductTerms::kArtistContractTermID];
            my $trackID = $termTrackData->[RPS::ArtistRoyalty::Fast::Static::ProductTerms::kTrackID];


            # Compare the sale date with the contract's term_start and term_end, if any.
            # Skip terms that don't fit the date.
            #
            my $termStart = $termTrackData->[RPS::ArtistRoyalty::Fast::Static::ProductTerms::kTermStart];
            my $termEnd = $termTrackData->[RPS::ArtistRoyalty::Fast::Static::ProductTerms::kTermEnd];

            if (($termStart && $termStart gt $saleDate)
             || ($termEnd && $termEnd lt $saleDate))
            {
                Log->info("skipping term: sale date out of range");
                next;
            }


            # If this is a 'Document'-type term, and we don't have a document price,  we skip the term.
            #
            if (RPS::DB::Item::ContractRateType::kRateTypePercentDocumentRetail == $termTrackData->[RPS::ArtistRoyalty::Fast::Static::ProductTerms::kRateTypeID]
             && 0 == $sale->[RPS::ArtistRoyalty::Fast::Static::Sales::kRetailPrice])
            {
                next;
            }
            
            if (RPS::DB::Item::ContractRateType::kRateTypePercentDocumentWholesale == $termTrackData->[RPS::ArtistRoyalty::Fast::Static::ProductTerms::kRateTypeID]
             && 0 == $sale->[RPS::ArtistRoyalty::Fast::Static::Sales::kWholesalePrice])
            {
                next;
            }



            # Can't just use contractID.  We also need to use track_id.
            # The same contract can land on the same album sale multiple times, and must be paid
            # out for each instance.
            #
            if ($contractID != $lastContractID || $trackID != $lastTrackID)
            {
                if ($lastContractID)
                {
                    # We've seen all possible terms for this contract, so we have a winner.
                    #
                    if (! $matchingTerm)
                    {
                        $matchingTerm = $defaultTerm;
                    }
                    if ($matchingTerm)
                    {
                        my $termID = $matchingTerm->[RPS::ArtistRoyalty::Fast::Static::ProductTerms::kArtistContractTermID];
                        my $trackID = $matchingTerm->[RPS::ArtistRoyalty::Fast::Static::ProductTerms::kTrackID];
                        if (!$paidMap{$lastContractID}{$trackID})
                        {
                            $self->_outputLine($incomeItemFD, $missedSalesFD, $incomeSourceID, $sale, $matchingTerm, $contractTermData,
                             $saleProductPrices);
                        }
                    }
                    else
                    {
                        $self->logMissedSale($missedSalesFD, $sale, RPS::ArtistRoyalty::Fast::Mapped::MissedSales::kNoDefaultTerm, $lastContractID);
                    }
                }
                $defaultTerm = undef;
                $matchingTerm = undef;
                $lastContractID = $contractID;
                $lastTrackID = $trackID;
            }


# !!! If we are iterating over the terms in priority order, then this logic will cause the 'lower' priority term to win.
# !!! In theory, if the terms are properly sorted, we'll see the default term first.
# !!! Then, if we _find_ a matching term, aren't we done?
#
            my $term = $contractTermData->{$termID};
            if (0 == $termTrackData->[RPS::ArtistRoyalty::Fast::Static::ProductTerms::kPriority])
            {
                $defaultTerm = $termTrackData;
                next;
            }

            # If the sale doesn't have a price_level, we use the product's default price level id
            #
            my $priceLevel = $sale->[RPS::ArtistRoyalty::Fast::Static::Sales::kPriceLevel()];
            if (0 == $priceLevel)
            {
                $priceLevel = $termTrackData->[RPS::ArtistRoyalty::Fast::Static::ProductTerms::kDefaultPriceLevelID];
            }

            if (! $matchingTerm 
             && (0 == $term->[RPS::ArtistRoyalty::Fast::Static::Terms::kRegionID()] 
                   || $regionCountryMap->{$term->[RPS::ArtistRoyalty::Fast::Static::Terms::kRegionID()]}{$sale->[RPS::ArtistRoyalty::Fast::Static::Sales::kCountryCode()]})
             && (0 == $term->[RPS::ArtistRoyalty::Fast::Static::Terms::kChannelID()] 
                   || $term->[RPS::ArtistRoyalty::Fast::Static::Terms::kChannelID()] == $sale->[RPS::ArtistRoyalty::Fast::Static::Sales::kChannel()])
             && (0 == $term->[RPS::ArtistRoyalty::Fast::Static::Terms::kPriceLevelID()] 
                   || $term->[RPS::ArtistRoyalty::Fast::Static::Terms::kPriceLevelID()] == $priceLevel)
             && ($sourceIDMap->{$incomeSourceID}{$term->[RPS::ArtistRoyalty::Fast::Static::Terms::kContractTermSourceID()]})
            )
            {
                $matchingTerm = $termTrackData;
                #
                # We've got a term. It wins.
            }
        }

        # Handle the last term outside of the loop.
        #
        if ($lastContractID)
        {
            # We've seen all possible terms for this contract, so we have a winner.
            #
            if (! $matchingTerm)
            {
                $matchingTerm = $defaultTerm;
            }
            if ($matchingTerm)
            {
                # JPK - Ok, NOW we can see whether we've already paid out on this term.
                #
                my $termID = $matchingTerm->[RPS::ArtistRoyalty::Fast::Static::ProductTerms::kArtistContractTermID];
                my $trackID = $matchingTerm->[RPS::ArtistRoyalty::Fast::Static::ProductTerms::kTrackID];

                if (!$paidMap{$lastContractID}{$trackID})
                {
                    $self->_outputLine($incomeItemFD, $missedSalesFD, $incomeSourceID, $sale, $matchingTerm, $contractTermData,
                     $saleProductPrices);
                }
            }
            else
            {
                $self->logMissedSale($missedSalesFD, $sale, RPS::ArtistRoyalty::Fast::Mapped::MissedSales::kNoDefaultTerm, $lastContractID);
            }
        }
    }
}

sub _openIncomeItemFileDescriptor
{
    my ($self, $outputDirectory) = @_;

    open OUTPUT, "> $outputDirectory/" . RPS::ArtistRoyalty::Fast::Mapped::Data->FileName() . "_$$.unsorted" or die "ERROR: $!";

    return *OUTPUT;
}

sub _openMissedSalesFileDescriptor
{
    my ($self, $outputDirectory) = @_;

    open MISSED, "> $outputDirectory/" . RPS::ArtistRoyalty::Fast::Mapped::MissedSales->FileName() . "_$$.unsorted" or die "ERROR: $!";

    return *MISSED;
}

sub _parseFirstLastSaleIndex
{
    my ($self, $rangeString) = @_;
    return unless $rangeString;

    my @bits = split(',', $rangeString);
    if (2 != scalar @bits || ($bits[1] <= $bits[0]))
    {
        $self->usage('ERROR: sale_range is invalid.');
    }
    return ($bits[0], $bits[1]);
}

sub logMissedSale
{
    my ($self, $outFD, $sale, $reason, $artistContractID) = @_;
    
    $artistContractID = 0 unless $artistContractID;

    my $description = '';

    # Add some details if this was a missing income source situation.
    # JPK - Expand this a bit.
    #
    if (RPS::ArtistRoyalty::Fast::Mapped::MissedSales::kNoIncomeSource == $reason)
    {
        $description = 'No income source id matches product type ' . $self->_idToSaleProductType($sale->[RPS::ArtistRoyalty::Fast::Static::Sales::kProductType])
         . ', format type ' . $self->_idToFormatType($sale->[RPS::ArtistRoyalty::Fast::Static::Sales::kFormatType]);
    }
    elsif (RPS::ArtistRoyalty::Fast::Mapped::MissedSales::kNoDefaultTerm == $reason)
    {
        my $incomeSourceID = $self->_determineIncomeSourceID($sale->[RPS::ArtistRoyalty::Fast::Static::Sales::kProductType], $sale->[RPS::ArtistRoyalty::Fast::Static::Sales::kFormatType]);
        my ($incomeSource, $formatFlag) = $self->_idToIncomeSourceAndPhysicalFlag($incomeSourceID);
        $description = 'No term found for country code ' . $sale->[RPS::ArtistRoyalty::Fast::Static::Sales::kCountryCode()]
         . ', income source ' . $incomeSource;
        if (1 == $formatFlag)
        {
            $description .=  ', price level ' . $self->_idToPriceLevel($sale->[RPS::ArtistRoyalty::Fast::Static::Sales::kPriceLevel()])
         . ', channel ' . $self->_idToChannel($sale->[RPS::ArtistRoyalty::Fast::Static::Sales::kChannel()]);
        }
    }
    elsif (RPS::ArtistRoyalty::Fast::Mapped::MissedSales::kNoPrice == $reason)
    {
        $description = 'No product price entry for price level ' . $self->_idToPriceLevel($sale->[RPS::ArtistRoyalty::Fast::Static::Sales::kPriceLevel()]);
    }


    my $formatString = 
#       '%12d'  # sale_id
       '%12d'  # product_id
     . "\t".'%12d'  # artist_contract_id
     . "\t".'%12d'  # reason code 
     . "\t".'%s'    # description
     . "\t".'%12d'  # units
#     . "\t".'%20.08f'  # revenue 
     . "\n";

    print $outFD sprintf($formatString, 
#        $sale->[RPS::ArtistRoyalty::Fast::Static::Sales::kSaleID()],
        $sale->[RPS::ArtistRoyalty::Fast::Static::Sales::kProductID()],
        $artistContractID,
        $reason,
        $description,
        ($sale->[RPS::ArtistRoyalty::Fast::Static::Sales::kSales] - $sale->[RPS::ArtistRoyalty::Fast::Static::Sales::kReturns]),
#        $sale->[RPS::ArtistRoyalty::Fast::Static::Sales::kTotalRevenue()],
    );
}

sub _outputLine
{
    my ($self, $outFD, $missedSalesFD, $incomeSourceID, $sale, $productTerm, $termData, $productPrices) = @_;

    my $term = $self->_getTermObj($productTerm, $termData);
    Log->info(" USING TERM:", $term);
    Log->info(" productPrices:", $productPrices);


    # JPK - If there isn't an actual product price, we need to know that so we can skip this sale.
    # (and log it at some point).
    #
    my ($priceLevel, $price, $rate, $prorate) = $term->getRateData($sale, $productTerm, $productPrices, $incomeSourceID);

    # !!! Rate needs to be rounded to 4 decimal places so lines will aggregate correctly with reserves.
    #
    $rate = Common::RSMath::round($rate, 4);

    Log->info(" priceLevel=", $priceLevel, ' price=', $price, ' rate=', $rate);

    if (! defined $price)
    {
        Log->info("no price returned to sale " . $sale->[RPS::ArtistRoyalty::Fast::Static::Sales::kSaleID()] . ", term " .  $term->[RPS::ArtistRoyalty::Fast::Static::Terms::kArtistContractTermID()]);
        $self->logMissedSale($missedSalesFD, $sale, RPS::ArtistRoyalty::Fast::Mapped::MissedSales::kNoPrice, $productTerm->[RPS::ArtistRoyalty::Fast::Static::ProductTerms::kArtistContractID()]);
        return;
    }

# !!! I can probably eliminate the padding

    my $formatString = 
       '%12d'  # payee_id
     . "\t".'%12d'  # album_id
     . "\t".'%12d'  # artist_contract_id
     . "\t". RPS::ArtistRoyalty::Fast::Mapped::Data::kTypeSale
     . "\t".'%12d'  # track_id
     . "\t".'%12d'  # term_id
     . "\t".'%1d'  # crossedflag
     . "\t".'%12d'  # region_id  (seems a little redundant?)
     . "\t".'%12d'  # income_source_id
     . "\t".'%12d'  # channel_id 
     . "\t".'%12d'  # price_level_id
     . "\t".'%20.08f'  # rate 
     . "\t".'%20.08f'  # price
     . "\t".'%20.08f'  # conversion_rate 
     . "\t".'%12d'  # sale_id  
     . "\t".'%12d'  # product_id  
     . "\t".'%12d'  # sales
     . "\t".'%12d'  # returns 
     . "\t".'%20.08f'  # revenue 
     . "\t".'%12d'  # units liquidated
     . "\t".'%20.08f'  # revenue liquidated 
     . "\t".'%1d'  # is digital flag
     . "\t".'%s'   # Using a string for now to see if I can replicate old code bug.  this should be a fixed point value.
     . "\t".'%12d'  # prorate track count (FB113)
     . "\n";

    print $outFD sprintf($formatString, 
     $term->[RPS::ArtistRoyalty::Fast::Static::Terms::kArtistPayeeID()], 
     $productTerm->[RPS::ArtistRoyalty::Fast::Static::ProductTerms::kAlbumID()],
     $productTerm->[RPS::ArtistRoyalty::Fast::Static::ProductTerms::kArtistContractID()],
     $productTerm->[RPS::ArtistRoyalty::Fast::Static::ProductTerms::kTrackID()],
     $term->[RPS::ArtistRoyalty::Fast::Static::Terms::kArtistContractTermID()], 
     $productTerm->[RPS::ArtistRoyalty::Fast::Static::ProductTerms::kCrossed()],
     $term->[RPS::ArtistRoyalty::Fast::Static::Terms::kRegionID()], 
     $incomeSourceID,
     $sale->[RPS::ArtistRoyalty::Fast::Static::Sales::kChannel()],
     $priceLevel,
     $rate,
     $price,
     $sale->[RPS::ArtistRoyalty::Fast::Static::Sales::kConversionRate()],
     $sale->[RPS::ArtistRoyalty::Fast::Static::Sales::kSaleID()],
     $sale->[RPS::ArtistRoyalty::Fast::Static::Sales::kProductID()],
     $sale->[RPS::ArtistRoyalty::Fast::Static::Sales::kSales()],
     $sale->[RPS::ArtistRoyalty::Fast::Static::Sales::kReturns()],
     $sale->[RPS::ArtistRoyalty::Fast::Static::Sales::kTotalRevenue()],
     0,
     0,
     $productTerm->[RPS::ArtistRoyalty::Fast::Static::ProductTerms::kIsDigital()],
     $sale->[RPS::ArtistRoyalty::Fast::Static::Sales::kDistributionFee()],
     $prorate, # FB113
     );
     
}





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;
    }
    elsif ('O' eq $formatType)
    {
        $incomeSourceID = RPS::DB::Item::IncomeSource::kIncomeSourcePerformanceIncome;
    }    
    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
        {
            Log->warn("WARNING - unable to determine income source id from productType $productType formatType $formatType");
        }

    }

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

    return $incomeSourceID;
}

sub _getTermObj
{
    my ($self, $productTerm, $termData) = @_;

    # !!! I don't want to waste time allocating memory to hold this data.
    # !!! I think I might be able to simply bless the term data reference.
    # !!! My goal here is to attach the correct methods to the data.
    #

    # Do I use the $termData anyplace else in this code?  It not, why have it here?
    #
    my $termID = $productTerm->[RPS::ArtistRoyalty::Fast::Static::ProductTerms::kArtistContractTermID];

    return RPS::ArtistRoyalty::Fast::TermType->GetTermObj($termID, $termData);

}


# JPK - Taken straight from the old code.
# Note that this is making DB::Item calls - I would rather extract this data.
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;
}

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

    if (! $gIncomeSourceIDMap)
    {
        $gIncomeSourceIDMap = {};
#         my $incomeSource = RPS::DB::Item::IncomeSource->Lookup(income_source_id => $incomeSourceID);


        my $all = RPS::DB::Item::IncomeSource->GetAll();
        while (my $item = $all->next())
        {
            $gIncomeSourceIDMap->{$item->income_source_id} = [ $item->description, $item->format ];
        }
    }

    my $name = '(Unknown)';
    my $format = 0;

    my $data = $gIncomeSourceIDMap->{$id};
    if ($data)
    {
        $name = $data->[0];
        $format = $data->[1];
    }

    return ($name, $format);
}


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 $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;
}


1;
