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

package RPS::Mechanical::UK::Process::CreateStatement::Mcps;


use strict;
use lib '/app/tools/rps/lib';
use lib '/app/tools/raptor/lib';
use lib '/app/tools/common/lib';
use Common::Assert;
use Common::RSApp;
use Common::RSMath;
use Common::DB::Item::ClientMcpsSalesPricingMode;
use Common::DB::Item::McpsDefaultRoyaltyRate;

use Raptor::DB::Item::Sale;

use RPS::Product::Price;
use RPS::Mechanical::UK::Process::CreateStatement;
use RPS::DB::Item::McpsLicense;
use RPS::DB::Item::McpsStatementItem;
use RPS::DB::Item::McpsStatement;
use RPS::DB::Item::Product;
use RPS::DB::Item::Album;
use RPS::DB::Item::Artist;
use RPS::DB::Item::Label;
use RPS::DB::Item::Channel;
use RPS::DB::Item::ProductPrice;
use RPS::DB::Item::ProductType;
use RPS::DB::Item::PriceLevel;
use RPS::DB::Item::Price;
use RPS::DB::Item::McpsExportUplift;
use RPS::DB::Item::McpsLicenseRetention;
use RPS::DB::Item::McpsLicenseCarryover;

use RPS::Mechanical::Process::SaleException;
use RPS::Mechanical::Process::Exception;


# This is the base for the DVD1 and AP1 statement processes, which are
# extremely similar.

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


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

    my $statement = RPS::DB::Item::McpsStatement->Lookup(mcps_statement_id => $self->_statementID());
    return $statement;
}



# Static methods.
#
sub commit
{
    my ($self) = @_;

    # Generally, I think the steps will be the same.
    #
    # I will need to iterate over all the statement items that belong to this statement
    # -> Fetching the items will probably be abstract.
    #
    my $allItems = RPS::DB::Item::McpsStatementItem->GetByMCPSStatementID($self->_statementID());
    while (my $item = $allItems->next())
    {
        $self->_commitItem($item);
    }

}


sub _commitItem
{
    my ($self, $item) = @_;

    # We're going to need the license data.
    #
    my $license = RPS::DB::Item::McpsLicense->Lookup(mcps_license_id => $item->mcps_license_id);


    # If we're holding retentions, create a new record for them.
    #
    # !!! We should only have 1 sort of retention being held.
    #
    my $retentionsHeld = $item->retentions();
    my $category = RPS::DB::Item::McpsLicense::kRetentionCategoryStandard;
    if ($item->tv_retentions() > 0)
    {
        $retentionsHeld = $item->tv_retentions();
        $category = RPS::DB::Item::McpsLicense::kRetentionCategoryTVAdvertised;
    }

    if ($retentionsHeld > 0)
    {
        # Figure out the correct period id, based on the number of completed
        # runs (which we have already incremented) and the initial value.
        #
        my $completedPeriods = _periodsCompletedForLicense($license);

        my $newRetention = RPS::DB::Item::McpsLicenseRetention->Create
        (
            mcps_license_id => $item->mcps_license_id,
            period_id => $completedPeriods + 1,
            units_held => $retentionsHeld,
            price => $item->price,
# !!! I may no longer need this column?
#            price_level_id =>
            retention_category => $category,
            original_statement_item_id => $item->mcps_statement_item_id,
        );
        $newRetention->save();
    }


    # Now look at carryover
    #
    if ($item->final_net_units < 0)
    {
        # !!! I believe this should _always_ be GB...
        my $newCarryover = RPS::DB::Item::McpsLicenseCarryover->Create
        (
            country_code => $item->country_code,
            mcps_license_id => $item->mcps_license_id,
            mcps_price_type => $item->mcps_price_type,
            price => $item->price,
            units => $item->final_net_units,
        );
        $newCarryover->save();
    }
}


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

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

    # This hash will be used to keep track of the McpsStatementItem references.
    #
    $self->{_items} = {};

    # Fetch the pricing scheme here.
    #
    my $modeItem = Common::DB::Item::ClientMcpsSalesPricingMode->Lookup(client_id => Common::RSApp::GetClientID());

    if ($modeItem)
    {
        $self->{_pricingMode} = $modeItem->mcps_sales_pricing_mode();
    }
    else
    {
        # Default...
        #
        $self->{_pricingMode} = Common::DB::Item::ClientMcpsSalesPricingMode::kPublishedDealerPricing;
    }


    # Build the retention rate table
    #
    $self->{_retentionRate} = {
        'S' => {
            1 => 0.1,
            2 => 0.1,
            3 => 0.1,
            4 => 0.1,
        },
        'T' => {
            1 => 0.25,
            2 => 0.25,
            3 => 0.1,
            4 => 0.1,
        },
    };


    # Let's also build the export uplift table.
    #
    my $allUplifts = RPS::DB::Item::McpsExportUplift->GetAll();
    while (my $upliftItem = $allUplifts->next())
    {
        $self->{_exportUplift}{$upliftItem->country_code()}{$upliftItem->product_type_id()} = $upliftItem->rate() / 100;
    }
    $self->_report("exportUplift table", 4);
    $self->_report($self->{_exportUplift}, 4);

    return $self;
}



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

    # Should be simple enough - just grab the correct license for this product id
    # from the McpsLicense table.
    #
#    my $license = RPS::DB::Item::McpsLicense->Lookup(payor_id => $self->_payorID(), product_id => $sale->product_id());

    # Return this as an array ref.
    #
    my @licenses;
    my $collection = RPS::DB::Item::McpsLicense->GetAll("SELECT * from mcps_license"
    . " WHERE payor_id =". $self->_payorID()
    . " AND product_id =". $sale->product_id());

    while (my $license = $collection->next())
    {
        push @licenses, $license;
    }

    return \@licenses;
}


sub _getStatementItemForSale
{
    my ($self, $license, $sale) = @_;
    assert($license);
    assert($sale);

    if ($sale->mcps_adjustment())
    {
        return $self->_getAdjustmentStatementItemForSale($license, $sale);
    }
    else
    {
        return $self->_getRegularStatementItemForSale($license, $sale);
    }
}



sub _getAdjustmentStatementItemForSale
{
    my ($self, $license, $sale) = @_;
    assert($license);
    assert($sale);

    my $countryCode = $sale->country_code();
    my ($price, $priceType) = $self->_getSalePrice($sale);

    return $self->_getStatementItem(
        license => $license,
        countryCode => $countryCode,
        price => $price,
        priceType => $priceType,
        isAdjustment => 1,
        comments => $sale->comments,
        saleID => $sale->sale_id,
    );
}


sub _getRegularStatementItemForSale
{
    my ($self, $license, $sale) = @_;
    assert($license);
    assert($sale);

    # Deleted state comes from the product, not the license.
    #
    my $product = RPS::DB::Item::Product->Lookup(product_id => $license->product_id);

    my $countryCode = $sale->country_code();
    my ($price, $priceType) = $self->_getSalePrice($sale, $product->deleted());

    # We usually leave saleID undef.
    # But if this is a deleted product, we need to report each sale on a seperate line.
    #
    my $saleID;
    my $deletedDate;
    if ($product->deleted())
    {
        $saleID = $sale->sale_id;
        $deletedDate = $product->deleted_date;
    }

    return $self->_getStatementItem(
        license => $license,
        countryCode => $countryCode,
        price => $price,
        priceType => $priceType,
        saleID => $saleID,
        deletedDate => $deletedDate,
    );
}

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

    my $license = $args{license};
    my $countryCode = $args{countryCode};
    my $price = $args{price};
    my $priceType = $args{priceType};
    my $adjustmentFlag = $args{isAdjustment};
    my $comments = $args{comments};
    my $deletedDate = $args{deletedDate};
    my $saleID = $args{saleID};
    $saleID = 'REGULAR' unless $saleID;

    # We need to pad/round the price, so '5' and '5.0000' hash the same.
    $price = Common::RSMath::round($price, 4);

    # Have we cached this item already?
    #
    # Notice the 'REGULAR' hash key.  We are going to combine the regular statement
    # item (where there will be just 1 for a given license/country/price/type) and
    # the adjustment items (where there will be more than 1, differentiated by sale_id).
    #
    if (! $self->{_items}{$license->mcps_license_id}{$saleID}{$priceType}{$price}{$countryCode})
    {
        my $item = $self->_createNewStatementItem($license, $price, $priceType, $adjustmentFlag, $comments, $countryCode, $deletedDate);

        $self->{_items}{$license->mcps_license_id}{$saleID}{$priceType}{$price}{$countryCode} = $item;
    }

    return $self->{_items}{$license->mcps_license_id}{$saleID}{$priceType}{$price}{$countryCode};
}


sub _createNewStatementItem
{
    my ($self, $license, $price, $priceType, $adjustmentFlag, $comments, $countryCode, $deletedDate) = @_;
    assert($license);
#    assert($price);
    assert($priceType);

    # Create it.  This means fetching the associated product so we can fill-in some state.
    #
    my $product = RPS::DB::Item::Product->Lookup(product_id => $license->product_id);
    die RPS::Mechanical::Process::Exception->new("Unable to find product " . $license->product_id . " associated with mcps_license " . $license->mcps_license_id) 
     unless $product;

    # !!! Sanity check - This should NOT be a track product...
    #
    die RPS::Mechanical::Process::Exception->new("Product " . $license->product_id . " associated with mcps_license " . $license->mcps_license_id . " is a track product!")
     if ($product->product_type_id == RPS::DB::Item::Product::kProductTypeDigitalTrack);


    # Fetch the album.
    #
    my $album = RPS::DB::Item::Album->Lookup(album_id => $product->asset_id())
     or die RPS::Mechanical::Process::Exception->new("Unable to load album " . $product->asset_id . " from product " . $license->product_id . " associated with mcps_license " . $license->mcps_license_id );

    # And fetch the artist.
    #
    my $artist = RPS::DB::Item::Artist->Lookup(artist_id => $album->artist_id())
     or die RPS::Mechanical::Process::Exception->new("ERROR - Unable to load artist " . $album->artist_id() . "from album" . $product->asset_id . " from product " . $license->product_id . " associated with mcps_license " . $license->mcps_license_id );


    # And the label info.
    #
    my $label = RPS::DB::Item::Label->Lookup(label_id => $album->label_id())
     or die RPS::Mechanical::Process::Exception->new("ERROR - Unable to load label" . $album->label_id() . "from album" . $product->asset_id . " from product " . $license->product_id . " associated with mcps_license " . $license->mcps_license_id );



    my $item = RPS::DB::Item::McpsStatementItem->Create
    (
        mcps_statement_id => $self->_statementID(),
        mcps_license_id => $license->mcps_license_id(),
        mcps_id => $license->mcps_id(),
        album_title => $album->title(),
        album_id => $album->album_id(),
        catalog_number => $album->catalog_number(),
        artist_name => $artist->name(),
        product_code => $product->product_code(),
        product_type_id => $product->product_type_id(),
        dvd_category => $license->dvd_category(),
        label_name => $label->label_name(),
        release_date => $product->release_date(),
        country_code => $countryCode,
        price => $price,
        mcps_price_type => $priceType,
        adjustment => $adjustmentFlag,
        comments => $comments,
        deleted => (defined $deletedDate ? 1 : 0),
        deleted_date => $deletedDate,
    );

    # Save the initial state - We want to make sure we have an id assigned.
    #
    $item->save();

    $self->_report("----- created this new statement item", 4);
    $self->_report($item, 4);

    return $item;
}


# Returns the correct price to use for a sale and product.
#
sub _getSalePrice
{
    my ($self, $sale, $deletedFlag ) = @_;
    assert($sale);

    my $price;
    my $priceType;

    my $conversionRate = $sale->conversion_rate();
    assert( $conversionRate, "Conversion rate must be > 0" );

    if ($deletedFlag)
    {
        # Deleted products will always report the 'retail price' found in the sale file.
        #
        $price = $sale->retail_price() * $conversionRate;

        $priceType = RPS::DB::Item::McpsStatementItem::kPriceTypeDeleted;
    }
    elsif (RPS::DB::Item::Channel::kChannelClub == $sale->channel
        || RPS::DB::Item::Channel::kChannelMailOrder == $sale->channel)
    {
        # 'Non-standard' sales channel.  We need a 'retail' price.
        # This will come from either the product's price table, or directly from the sales file if
        # the system is configured that way.
        #
        $priceType = RPS::DB::Item::McpsStatementItem::kPriceTypeRetail;

        if (Common::DB::Item::ClientMcpsSalesPricingMode::kSaleDocumentPricing eq $self->{_pricingMode})
        {
            $price = $sale->retail_price() * $conversionRate;
        }
        elsif ($sale->price_level)
        {
            $price = RPS::Product::Price::GetRetailProductPrice($sale->product_id, $sale->price_level);
        }
    }
    else
    {
        $priceType = RPS::DB::Item::McpsStatementItem::kPriceTypeDealer;

        if (Common::DB::Item::ClientMcpsSalesPricingMode::kSaleDocumentPricing eq $self->{_pricingMode})
        {
            $price = $sale->wholesale_price() * $conversionRate;
        }
        elsif ($sale->price_level)
        {
            $price = RPS::Product::Price::GetWholesaleProductPrice($sale->product_id, $sale->price_level);
        }
#        $priceType = RPS::DB::Item::McpsStatementItem::kPriceTypeDealer;
#        $price = $self->_getProductPrice($sale->product_id, $sale->price_level);
    }

    # Let's blow up if we don't have a price (and it's not a free or promo sale)...
    #
    my $priceLevel = 0;
    if ($sale->price_level)
    {
        $priceLevel = $sale->price_level;
    }

    if (0 == $price && ! $sale->free && $priceLevel != RPS::DB::Item::PriceLevel::kPriceLevelPromo)
    {
        # !!! This would be a good place for an exception class.
        #
        die RPS::Mechanical::Process::SaleException::NoPrice->new($sale, 'Unable to determine the price for this sale');
    }

    return (Common::RSMath::round($price, 4), $priceType);
}


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


    # !!! Should this be checking retail price?  !!!!
    # Determine which 'buckets' to update.
    #
    if ($sale->free || 0 == $sale->retail_price)
    {
        # This is a promo sale.
        #
        $self->_report(" sale is 'free', adding units to promo_units AND gross_units", 4);
        $statementItem->promo_units( $statementItem->promo_units() + $sale->sales() );
        $statementItem->gross_units( $statementItem->gross_units() + $sale->sales() );
    }
    else
    {
        # A regular sort of sale line, with sales and returns.
        #
        $self->_report(" sale is 'normal', adding units to gross_units", 4);
        $statementItem->gross_units( $statementItem->gross_units() + $sale->sales() );

        my $clientID = Common::RSApp::GetClientID();

        # If this is a 'foreign' sale, we do not allow returns...
        #
        if ('GB' eq $sale->country_code())
        {
            $statementItem->return_units( $statementItem->return_units() + $sale->returns() );
        }
        elsif( 'IE' eq $sale->country_code() && 182 == $clientID ) # 182 = MOS
        {
            # ..unless you're Ministry of Sound, in which case MCPS says you
            # can also report on Ireland sales in addition to GB sales.  See
            # FB12504 for the details.
            $statementItem->return_units( $statementItem->return_units() + $sale->returns() );
        }
    }
}


# Re-write this whole function from scratch.
# We need to take the consolidation into account as we go.
#
sub _finalize
{
    my ($self) = @_;

    $self->_report("ITEM DUMP: ", 4);
    $self->_report($self->{_items});

    # Note that this will return a new hash, since the prices might change.
    #
    $self->{_items} = $self->_calculateNet($self->{_items});

    $self->_calculateFinalNet($self->{_items});

    # All done messing about with the items - Save them out to the DB.
    #
    $self->_saveItems($self->{_items});
}


sub _saveItems
{
    my ($self, $inHash) = @_;

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

    # This seems like a good place to calculate the statement_level estimates.
    #
    my $netUnits;
    my $grossUnits;
    my $estimatedLiability;
    my $retentionsHeld;
    my $retentionsLiquidated;
    my $royaltyPayableUnits;


    # We'll need to read in the rate table.
    #
    my %rateTable;

    my $allRates = Common::DB::Item::McpsDefaultRoyaltyRate->GetAll();
    while (my $rateItem = $allRates->next())
    {
        $rateTable{$rateItem->mcps_sale_channel_id}{'AP1'} = $rateItem->ap1_rate();
        $rateTable{$rateItem->mcps_sale_channel_id}{'A'} = $rateItem->cat_a_rate();
        $rateTable{$rateItem->mcps_sale_channel_id}{'B'} = $rateItem->cat_b_rate();
        $rateTable{$rateItem->mcps_sale_channel_id}{'C'} = $rateItem->cat_c_rate();
        $rateTable{$rateItem->mcps_sale_channel_id}{'AVP'} = $rateItem->cat_avp_rate();
    }


    foreach my $licenseID (keys %$inHash)
    {
        foreach my $saleID (keys %{$inHash->{$licenseID}})
        {
            foreach my $priceType (keys %{$inHash->{$licenseID}{$saleID}})
            {
                foreach my $price (keys %{$inHash->{$licenseID}{$saleID}{$priceType}})
                {
                    foreach my $countryCode (keys %{$inHash->{$licenseID}{$saleID}{$priceType}{$price}})
                    {
                        $self->_report(" looking at licenseID $licenseID saleID $saleID priceType $priceType price $price countryCode $countryCode", 3);

                        my $item = $inHash->{$licenseID}{$saleID}{$priceType}{$price}{$countryCode};
                        $item->save();

                        $self->_report($item, 4);


                        my $category = $item->dvd_category();
                        $category = 'AP1' unless $category;

                        # This channel_id logic is a bit squirrly...
                        #
                        my $channelID;
                        $channelID = '1' if $item->mcps_price_type eq 'D';
                        $channelID = '2' if $item->mcps_price_type eq 'R';
                        $channelID = '2' if $item->mcps_price_type eq 'L';
                        $channelID = '3' if $item->mcps_price_type eq 'I';

                        my $rate = $rateTable{$channelID}{$category};

                        # !!! Might need to re-visit these calculations.
                        #
                        $netUnits += $item->final_net_units;
                        $grossUnits += $item->gross_units;

                        # Net negative units are carried over to the next
                        # accounting period, so they should not be included
                        # in the Estimated Liability figure.
                        #
                        if ($item->final_net_units > 0 )
                        {
                            $estimatedLiability += Common::RSMath::round(($item->final_net_units * (($rate / 100) * $item->price)), 2);
                            $royaltyPayableUnits += $item->final_net_units;
                        }

                        $retentionsHeld += $item->retentions if ($item->retentions > 0);
                        $retentionsHeld += $item->tv_retentions if ($item->tv_retentions > 0);

                        $retentionsLiquidated += (-1 * $item->retentions) if ($item->retentions < 0);
                        $retentionsLiquidated += (-1 * $item->tv_retentions) if ($item->tv_retentions < 0);
                    }
                }
            }
        }
    }

    my $statement = $self->_getStatementDBItem();
    $statement->net_units($netUnits);
    $statement->gross_units($grossUnits);
    $statement->estimated_liability($estimatedLiability);
    $statement->retentions_held($retentionsHeld);
    $statement->retentions_liquidated($retentionsLiquidated);
    $statement->royalty_payable_units($royaltyPayableUnits);

    $statement->save();
}


sub _calculateNet
{
    my ($self, $inHash) = @_;

    my %outHash;

    foreach my $licenseID (keys %$inHash)
    {
        my $license = RPS::DB::Item::McpsLicense->Lookup(mcps_license_id => $licenseID);
        my $product = RPS::DB::Item::Product->Lookup(product_id => $license->product_id);

        # 'regular' items will have a 'sale_id' of 'REGULAR'.
        # If that is not the sale id, then we're looking at an adjustment.
        # I'm hashing the sale_id up here, so that all the 'normal' items will
        # naturally cluster together.
        #
        foreach my $saleID (keys %{$inHash->{$licenseID}})
        {
            foreach my $priceType (keys %{$inHash->{$licenseID}{$saleID}})
            {
                foreach my $price (keys %{$inHash->{$licenseID}{$saleID}{$priceType}})
                {
                    foreach my $countryCode (keys %{$inHash->{$licenseID}{$saleID}{$priceType}{$price}})
                    {
                        my $item = $inHash->{$licenseID}{$saleID}{$priceType}{$price}{$countryCode};

                        $self->_report("_calculateNet - looking at item", 4);
                        $self->_report($item, 4);

                        $self->_sumItemAndTakeRetentions($item);


                        my $newPrice = $price;

                        # export allowance?
                        #
                        if (RPS::DB::Item::McpsStatementItem::kPriceTypeDealer eq $priceType
                         && 'GB' ne $countryCode
                         && 'REGULAR' eq $saleID
                         && $item->net_units() > $self->_exportAllowance())
                        {
                            # Look for the explicit rate first.
                            # If we can't find one, look for the 'all physical products' version.
                            my $rate = $self->{_exportUplift}{$countryCode}{$product->product_type_id()};
                            if(! $rate)
                            {
                                $rate = $self->{_exportUplift}{$countryCode}{RPS::DB::Item::ProductType::kAllPhysicalProducts};
                            }

                            $self->_report("  item has triggered export allowance : rate = $rate", 4);

                            if ($rate && $rate != 1)
                            {
                                $newPrice = Common::RSMath::round( $item->price() * $rate, 4);  # Why 4 decimal places, anyway? !!!
                                $item->price($newPrice);
                            }
                        }


                        # Save the item in the new hash
                        #
                        $outHash{$licenseID}{$saleID}{$priceType}{$newPrice}{$countryCode} = $item;
                    }
                }
            }
        }
    }

    return \%outHash;
}


sub _calculateFinalNet
{
    my ($self, $inHash) = @_;

    foreach my $licenseID (keys %$inHash)
    {
        my $license = RPS::DB::Item::McpsLicense->Lookup(mcps_license_id => $licenseID);
        my $product = RPS::DB::Item::Product->Lookup(product_id => $license->product_id);

        # 'regular' items will have a 'sale_id' of 'REGULAR'.
        # If that is not the sale id, then we're looking at an adjustment.
        # I'm hashing the sale_id up here, so that all the 'normal' items will
        # naturally cluster together.
        #
        foreach my $saleID (keys %{$inHash->{$licenseID}})
        {
            foreach my $priceType (keys %{$inHash->{$licenseID}{$saleID}})
            {
                foreach my $price (keys %{$inHash->{$licenseID}{$saleID}{$priceType}})
                {
                    # So here's the wacky part:
                    # If these are 'regular' items,
                    # and _any_ of them have a negative net,
                    # then we want to consolidate all the units into the GB line.
                    # Otherwise, we can pretty much leave them alone.
                    #
                    my $consolidateFlag = 0;
                    my $lumpSum = 0;
                    my $gbItem = undef;
                    foreach my $countryCode (keys %{$inHash->{$licenseID}{$saleID}{$priceType}{$price}})
                    {
                        my $item = $inHash->{$licenseID}{$saleID}{$priceType}{$price}{$countryCode};

                        $self->_report("_calculateFinalNet - looking at item", 4);
                        $self->_report($item, 4);

                        if ('GB' eq $countryCode)
                        {
                            $gbItem = $item;
                        }
                        $item->final_net_units($item->net_units() - $item->tv_retentions() - $item->retentions());
                        if ($item->final_net_units() < 0)
                        {
                            $consolidateFlag = 1;
                        }
                        $lumpSum += $item->final_net_units();
                    }


                    # Looks like 'consolidation' is not actually allowed!
                    # For now, I will leave in this logic, but I'll disable it.
                    #
                    $consolidateFlag = 0; # !!! Skipping the consolidation logic !!!

                    if ('REGULAR' eq $saleID && $consolidateFlag)
                    {
                        $self->_report("_calculateFinalNet - Consolidating net units of $lumpSum - license id: $licenseID  priceType: $priceType  price: $price ", 4);

                        # This seems unlikely, but we might not _have_ a GB item.  In this case, then, we'd need
                        # to forge one.
                        #
                        if (! defined $gbItem)
                        {
                            $self->_report("_calculateFinalNet !!! Making up a GB item", 4);

                            $gbItem = $self->_createNewStatementItem($license, $price, $priceType, 0, '', 'GB', undef);
                            $inHash->{$licenseID}{'REGULAR'}{$priceType}{$price}{'GB'} = $gbItem;
                        }

                        foreach my $countryCode (keys %{$inHash->{$licenseID}{$saleID}{$priceType}{$price}})
                        {
                            my $item = $inHash->{$licenseID}{$saleID}{$priceType}{$price}{$countryCode};
                            if ('GB' eq $countryCode)
                            {
                                $item->final_net_units($lumpSum);
                            }
                            else
                            {
                                $item->final_net_units(0);
                            }
                        }
                    }
                }
            }
        }
    }
}



sub _sumItemAndTakeRetentions
{
    my ($self, $item) = @_;

    my $licenseID = $item->mcps_license_id();
    my $license = RPS::DB::Item::McpsLicense->Lookup(mcps_license_id => $licenseID);


    my $completedPeriods = _periodsCompletedForLicense($license);

    # calculate the 'net' units.
    #
    $item->net_units($item->gross_units + $item->prior_units - $item->promo_units - $item->return_units);

    # take retentions if sales > returns, and we retain on this item.
    # !!! Retentions come off of _net_ numbers.
    # !!! Only take retentions if the net_units > 10, not 0
    #
    if ('GB' eq $item->country_code()
     && $completedPeriods < 4
     && $item->mcps_price_type eq RPS::DB::Item::McpsStatementItem::kPriceTypeDealer
     && $item->net_units() >= 10)
    {
        # TV or Regular?
        # !!! Don't we have a table someplace that holds the retention schedule?
        # !!! For now I will build one in the constructor, which will at least make that
        # !!! abstract here.
        #
        my $period = $completedPeriods + 1;
        if (RPS::DB::Item::McpsLicense::kRetentionCategoryTVAdvertised eq $license->retention_category())
        {
            my $rate = $self->{_retentionRate}{RPS::DB::Item::McpsLicense::kRetentionCategoryTVAdvertised()}{$period};
            $item->tv_retentions(Common::RSMath::round(($item->net_units() * $rate), 0));

            $self->_report("_sumItemAndTakeRetentions:  taking TV Advertised retentions, rate $rate", 4);
        }
        else
        {
            my $rate = $self->{_retentionRate}{RPS::DB::Item::McpsLicense::kRetentionCategoryStandard()}{$period};
            $item->retentions(Common::RSMath::round(($item->net_units() * $rate), 0));

            $self->_report("_sumItemAndTakeRetentions:  taking regular retentions, rate $rate", 4);
        }
    }
    else
    {
        $self->_report("_sumItemsAndTakeRetentions: NOT TAKING RETENTIONS", 4);
    }

    # !!! Shouldn't we be dealing with liquidations here?
    # !!! Well, we might have licenses that had no sales, but are in their final period and
    # !!! need to be liquidated.
    #
    # !!! So, in theory, by the time we get here, if retentions were released, there should be a
    # _negative_ value in the 'retentions' column.
    #
    $self->_report("_sumItemAndTakeRetentions:  exiting", 4);
    $self->_report($item, 4);

}

sub _getStatementItemForRetention
{
    my ($self, $retention) = @_;

    my $licenseID = $retention->mcps_license_id;
    my $license = RPS::DB::Item::McpsLicense->Lookup(mcps_license_id => $licenseID);


    my $priceType = RPS::DB::Item::McpsStatementItem::kPriceTypeDealer;


    # If this is a 'historical' reserve (entered in via the UI), then we won't actually
    # _have_ the price in the table.  And we'll therefore need to use the wholesale price.
    # Note that on commit, we'll need to fill-in the price : We want to make sure we keep
    # the price we originally retained.
    my $price = $retention->price();
    if (0 == $price)
    {
        $price = RPS::Product::Price::GetWholesaleProductPrice($license->product_id, $retention->price_level_id);
    }


    my $countryCode = 'GB';

    return $self->_getStatementItem(
        license => $license,
        countryCode => $countryCode,
        price => $price,
#        priceType => $retention->mcps_sales_pricing_mode(),
        priceType => $priceType,
    );

}

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

    return $self->_getStatementItem(
        license => $license,
        countryCode => $carryover->country_code,
        price => $carryover->price,
        priceType => $carryover->mcps_price_type,
    );

}

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

    my $completedPeriods = _periodsCompletedForLicense($license);

    if (4 == $completedPeriods)
    {
        my $collection = RPS::DB::Item::McpsLicenseRetention->GetByMcpsLicenseID($license->mcps_license_id);
        while (my $retention = $collection->next())
        {
            $self->_liquidateRetention($retention);
        }
    }
}

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

    # See whether we had negative units held over from a previous run.
    #
    my $carryovers = RPS::DB::Item::McpsLicenseCarryover->GetAllForLicense($license->mcps_license_id);
    while (my $carryover = $carryovers->next())
    {
        my $item = $self->_getStatementItemForLicenseCarryover($license, $carryover);
        $item->prior_units( $item->prior_units() + $carryover->units() );
    }
}


sub _periodsCompletedForLicense
{
    my ($license) = @_;

    my $completedPeriods = RPS::DB::Item::McpsLicenseRun->NumCommittedRuns($license->mcps_license_id);
    $completedPeriods += $license->initial_periods_completed();
    return $completedPeriods;
}

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

    die "ERROR - You must overload _getProcessedSales";
}

sub _getUnprocessedSales
{
    my ($self, $sales) = @_;

    # Fetch the sales that have already been processed; we need to exclude
    # these from further processing
    #
    my $col = $self->_getProcessedSales();
    my %processed;
    while( $col->hasNext() )
    {
       my $runMapItem = $col->next();
       $processed{ $runMapItem->sale_id } = 1;
    }

    # Build the set of unprocessed sales
    #
    my @unprocessedSales;
    foreach my $id (@$sales)
    {
       next if ( exists $processed{$id} );
       push @unprocessedSales, $id;
    }

    return \@unprocessedSales;
}

1;
