package RPS::Import::Importer;

use strict;

# When adding/modifying an importer, edit the following files:
#   data_classes/lib/Client/Service.pm
#   sale_import/lib/Sale/File.pm
#   sale_import/lib/Import/Service.pm
#   sale_import/lib/Import/<FILE>

use Date::Calc qw(Add_Delta_Days Days_in_Month);
use Time::Local;
use Unicode::Normalize;
use Data::Dumper;

use lib '/app/tools/data_classes/lib/';
use Client::Service;

use lib '/app/tools/sale_import/lib/';
use Import::ValidationError;

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

use lib '/app/tools/metadata/lib';
use Validate::Util;

use lib '/app/tools/common/lib';
use Common::RSApp;
use Common::Client;
use Common::Locale;
use Common::Consts;
use Common::Country;
use Common::Log;
use Common::Assert;
use Common::CurrencyFormat;
use Common::Util;

use lib '/app/tools/rps/lib';
use RPS::DB::Item::MediaType;
use RPS::Sale::Match;
use RPS::File::Sale;
use RPS::Import::SaleRec;

use base 'Import::Importer';

use constant WMG => 21;

my $SERVICE_DECORATION = '_(mobile|wireless|networks?|media)$';
my $SERVICE_MAP = {};

# Valid accessor fields.  Lists all the valid accessors that can be used in the
# _getOffset method.
sub _validAccessors {
	qw(
	    albumName artistName averagePrice channel clientProductID configuration
        conversionRate countryCode currencyCode free importStatus isrc labelName
        outlet payoutType price wholesalePrice retailPrice priceLevel priceType
        retail returns returnsRevenue sales salesRevenue serviceID serviceProductID
        totalRevenue trackName trackNum units upc wholesaleRate mcps_adjustment
        comments mediaType productType formatType dateBegin dateEnd type
	)
}

# Sale record accessors
#
#  These methods mirror fields in the RPS::Import::SaleRec struct, infact they
#  will be used to populate that struct.
#
#  The default behavior is to pull the data from the column using an offset.  However,
#  the methods can be overloaded for trickier imports.
#
#  There are a couple exceptions to the default behavior.
#    - mediaType, productType, and formatType are generally identified by a single field
#      so we handle those with a more complex method.
#    - dateBegin and dateEnd usually need special formatting so don't use the default
#      accessor methods.

sub albumName             { shift->_getByFieldName( 'albumName' ) }
sub artistName            { shift->_getByFieldName( 'artistName' ) }
sub averagePrice          { shift->_getByFieldName( 'averagePrice' ) }
sub channel               { shift->_getByFieldName( 'channel' ) }
sub clientProductID       { shift->_getByFieldName( 'clientProductID' ) }
sub configuration         { shift->_getByFieldName( 'configuration' ) }
sub conversionRate        { shift->_getByFieldName( 'conversionRate' ) }
sub countryCode           { shift->_getByFieldName( 'countryCode' ) }
sub currencyCode          { shift->_getByFieldName( 'currencyCode' ) }
sub free                  { shift->_getByFieldName( 'free' ) }
sub importStatus          { shift->_getByFieldName( 'importStatus' ) }
sub isrc                  { shift->_getByFieldName( 'isrc' ) }
sub labelName             { shift->_getByFieldName( 'labelName' ) }
sub outlet                { shift->_getByFieldName( 'outlet' ) }
sub payoutType            { shift->_getByFieldName( 'payoutType' ) }
sub price                 { shift->_getByFieldName( 'price' ) }
sub wholesalePrice        { shift->_getByFieldName( 'wholesalePrice' ) }
sub retailPrice           { shift->_getByFieldName( 'retailPrice' ) }
sub priceLevel            { shift->_getByFieldName( 'priceLevel' ) }
sub priceType             { shift->_getByFieldName( 'priceType' ) }
sub retail                { shift->_getByFieldName( 'retail' ) }
sub returns               { shift->_getByFieldName( 'returns' ) }
sub returnsRevenue        { shift->_getByFieldName( 'returnsRevenue' ) }
sub sales                 { shift->_getByFieldName( 'sales' ) }
sub salesRevenue          { shift->_getByFieldName( 'salesRevenue' ) }
sub serviceProductID      { shift->_getByFieldName( 'serviceProductID' ) }
sub totalRevenue          { shift->_getByFieldName( 'totalRevenue' ) }
sub trackName             { shift->_getByFieldName( 'trackName' ) }
sub trackNum              { shift->_getByFieldName( 'trackNum' ) }
sub units                 { shift->_getByFieldName( 'units' ) }
sub upc                   { shift->_getByFieldName( 'upc' ) }
sub wholesaleRate         { shift->_getByFieldName( 'wholesaleRate' ) }
sub mcps_adjustment       { shift->_getByFieldName( 'mcps_adjustment' ) }
sub comments              { shift->_getByFieldName( 'comments' ) }

# These accessors don't call the default accessor method because they are usually
# determined by 1 field, saleType.
sub mediaType             { shift->_getBySaleType( 'mediaType' ) }
sub productType           { shift->_getBySaleType( 'productType' ) }
sub formatType            { shift->_getBySaleType( 'formatType' ) }

# saleType is a special field, not in the sale record, that is used when we
# use a single field to determin mediaType, productType and formatType.
sub type                  { shift->_getByFieldName( 'type' ) }


# This is the class we use for sale table access, by default.
#
sub _defaultSaleObjectClass { return 'RPS::File::Sale'; }

sub _instantiateDBO
{
    my ($self) = @_;
    assert($self->{clientID});

    return Common::RSDB->new(client_id => $self->{clientID});
}

# this an importer for physical or digital products
sub physical {}

# pass the potentially ugly service name from a sales file and get a service_id back
# perl -MImport::Importer -le "print Import::Importer->getServiceID('SERVICE_NAME')"
sub getServiceID
{
    my $self    = shift;
    my $service = lc $_[0];

    # Strip out diacriticals and other unamericanisms.
    # This two step procedure will first decompose these characters into their
    # component parts (i.e. the letter and the marks), then strips out the marks.
    #
    $service = Unicode::Normalize::NFKD($service);
    $service =~ s/\p{NonspacingMark}//g;

    $service =~ s/^\s+//;
    $service =~ s/\s+$//;
    $service =~ s/,?\s+(inc(orporated)?|llc|corp(oration)?)\.?\b//;
    $service =~ s/^t[-\s]+(mobile|online)/t$1/; # t-mobile, t-online
    $service =~ s/\s*-a-\s*/a/;                 # dig-a-dub
    $service =~ s/\s*&\s*/_/;                   # mix & burn, at&t
    $service =~ s/\s*-\s*/_/g;                  # all others with a '-' become '_'
    $service =~ s/\.com//;
    $service =~ s/\s*[^\w\s]+\s*//g;
    $service =~ s/\s+/_/g;

    return unless $service;

    _getServiceMap() unless keys %$SERVICE_MAP;

    my $altService;
    ($altService = $service) =~ s/$SERVICE_DECORATION//;
    return $SERVICE_MAP->{$service} || $SERVICE_MAP->{$altService} || undef;
}

# pass the potentially ugly format name from a sales file and get the RS format_id back
# perl -MImport::Importer -le "print Import::Importer->getWirelessFormatID('FORMAT_NAME')"
#
# this was originally done for WMG but a more robust solution now exists in
# Import::WMG::Util as getWirelessFormatTypeID
# no other clients currently distinguish between the various wireless formats but we'll
# leave it here for now in case that changes
sub getWirelessFormatID
{
    my $self   = shift;
    my $format = lc $_[0];

    return
        $format =~ /voice/      ? RPS::File::Sale::FORMAT_RINGTONE : # was FORMAT_VOICERINGER (FB1324)
        $format =~ /hi\W?fi/    ? RPS::File::Sale::FORMAT_RINGTONE : # was FORMAT_MASTERTONE (FB1324)
        $format =~ /phonic/     ? RPS::File::Sale::FORMAT_RINGTONE : # was FORMAT_MIDI (FB1324)
        $format =~ /ringback/   ? RPS::File::Sale::FORMAT_RINGBACK :
        #$format =~ /(wallpaper|graphic|screen.?saver)/ ? RPS::File::Sale::FORMAT_GRAPHIC : # retired (FB1324)
        $format =~ /video/      ? RPS::File::Sale::FORMAT_RINGTONE : # was FORMAT_VIDEORINGER (FB1324)
        $format =~ /tone/       ? RPS::File::Sale::FORMAT_RINGTONE : # was FORMAT_MASTERTONE (FB1324)
        ''
    ;
}

sub numericToDate
{
    my $self = shift;
    my @date = Add_Delta_Days(1900,1,1,shift);
    return wantarray ? @date : sprintf("%d-%02d-%02d", @date);
}

# !!! This will have to be updated to be language-aware as well.
# Some countries use ',' for decimals and '.' or ' ' or '`' for their
# thousands seperator...
#
sub numeric
{
    my $self = shift;
    my $number = join('', @_);

    $number =~ s/^[^\d\-\.]*//;
    my $negative = $number =~ s/^-// ? 1 : 0;

    $number =~ s/,(\d\d\d)/$1/g;
    $number =~ s/,/./; # should only be 1 leftover comma at this point (if any) which should be a decimal pt
    $number =~ s/[^\d\.]+//g;
    $number =~ s/\.00$//;

    $number *= -1 if $negative;

    return $number =~ m/^-?\d*\.?\d+$/ ? $number : undef;
}

sub is_wmg
{
    my $self = shift;
    return $self->{clientID} == WMG ? 1 : 0;
}

#
# Private methods
#

sub _insertSale
{
    my $self = shift;
    my %args = @_;

    my $saleRec = $args{sale};

    $saleRec->mediaType(Raptor::DB::Item::Sale::kMediaTypeDefault()) if ($saleRec->mediaType eq '');

    # Keep track of the service id, if there is one.
    # We'll need to know this during post-processing.
    #
    $self->{services}{$saleRec->serviceID} = 1 if ($saleRec->serviceID);

    if ($saleRec->serviceID == 5 && $saleRec->formatType eq RPS::File::Sale::FORMAT_DOWNLOAD) {
        $saleRec->formatType(RPS::File::Sale::FORMAT_VPD);
    }

    # We need to remap some older or incorrect country codes to the correct country codes en masse.
    # So we'll use a little hash and swap where appropriate, this will allow for the expantion
    # of the substitutions where necessary.
    my %countriesFixed = (
        FX  => 'FR',
        JA  => 'JM',
        UK  => 'GB',
        YU  => 'MK',
    );

    $saleRec->countryCode($countriesFixed{$saleRec->countryCode}) if (defined $countriesFixed{$saleRec->countryCode});

    # If the derived class didn't set the currencyCode explicitly, we'll default to 'USD'.
    # - Yes, 'USD', even if that isn't our client's base currency.  This is because we're
    # assuming that the _file_ came from a US source.
    #
    # !!! I am _VERY_ tempted to remove this bit of code.  We should set the currency code in every importer, period.
    #
    if (! defined $saleRec->currencyCode)
    {
        $saleRec->currencyCode('USD');
    }
        
    # Similar to what we do for country codes above, we'll map currency codes to what they should be here.
    my %currenciesFixed = (
        RMB => 'CNY',
    );

    $saleRec->currencyCode($currenciesFixed{$saleRec->currencyCode}) if (defined $currenciesFixed{$saleRec->currencyCode});    
    
    # !!! We do this exact same test later in _validateCurrencyCode so let's skip it at this point.
    #
    #else
    #{
    #    # Make sure the currency_code is _VALID_.
    #    if (! Common::CurrencyFormat::CodeIsValid($saleRec->currencyCode))
    #    {
    #        die Import::ValidationError->new($saleRec, "currencyCode '" . $saleRec->currencyCode . "' is not recognized");
    #    }
    #}


    # Check for our 'native' currency code here, and adjust
    # conversion_rate if necessary.
    #
    # This will essentially override any specific logic in the derived classes
    # that pertained to setting the conversion rate.
    #
    # But, don't bother to do this if the conversionRate has already been set.
    # !!! Actually, we will _always_ override conversionRate now.
    #
#    if (! defined $saleRec->conversionRate())
#    {
        my $nativeCurrency = Common::Client::Current()->Locale()->currencyFormat()->currencyCode();
        if ($saleRec->currencyCode ne $nativeCurrency)
        {
            $saleRec->conversionRate(0);
        }
        else
        {
            $saleRec->conversionRate(1);
        }
#    }

    $self->_checkForRevenueWithoutUnits($saleRec);

    $self->_sanitizeFormatType($saleRec);

    # Before we validate, we want to convert any small numbers that have ended up in 'scientific notation'
    # back into strings that contain the value formatted as a normal number again.
    # Basically, any value < '0.00001' or so gets mangled into the '1e-05' notation.
    #
    # Many of our importers need to do division.  For example, to calculate the actual price when
    # just given revenue and units.  If the revenue is small, we end up with a value Perl will squash.
    #
    # So, to make life easier, we'll 'expand' these values here.
    #
    ## !!! Accessor functions _can_ be re-defined, according to the docs.
    #
#    foreach my $floatField ('averagePrice', 'wholesalePrice', 'returnsRevenue', 'salesRevenue', 'totalRevenue')
#    {
#        Common::Log::Print("floatField: $floatField.  Value = " . $saleRec->$floatField);
#        # Let's not just convert _everything_ : We want to complain if they've stuffed a non number in here...
#        # And I am going to limit this to _small_ numbers.
#        #
#        if (defined $saleRec->$floatField()
#         && $saleRec->$floatField() =~ /\d+\.\d+e-\d+/)
#        {
#            $saleRec->$floatField(sprintf("%.8f", $saleRec->$floatField()));
#            Common::Log::Print("   CONVERTED floatField: $floatField.  Value = " . $saleRec->$floatField());
#        }
#    }
#

    # Now we can validate the saleRec as best we can.
    # This will throw an exception if validation fails.
    #
   	$self->_validateSaleRec($saleRec);


    # !!! This passing a 'DBO' reference is a silly waste of time.
    # !!! We've got a cached client database reference in the Application already.
    #
    my $sale = $self->_saleObjectClass()->CreateFromSaleRec(saleRec => $saleRec, file => $self->{_file}, hasError => $self->{_hasError});
    # Why bother with a 'save' method, since we don't do anything with the object.  Let's just make
    # the CreateFromSaleRec method do it all.
#    $saleDB->Save();

	# Ok, now we need to go through and fill in the sale id for any sale_import_error and sale_import_error_raw_value
	# entries we created for this sale -- IF this is an importer that has opted in to MapFace.
	$self->_fillInSaleID($sale->SaleID())
}

sub _fillInSaleID
{
	# The MapFace base class overloads this and actually does something with the sale ID.	
}

sub _printSale
{
    my $self = shift;
    my %args = @_;

    my $sale = $args{sale};

    warn join( "\t",
        $sale->lineNum,         $sale->productType,
        $sale->formatType,      $sale->dateBegin,
        $sale->dateEnd,         $sale->serviceProductID,
        $sale->clientProductID, $sale->upc,
        $sale->isrc,            $sale->artistName,
        $sale->albumName,       $sale->trackName,
        $sale->labelName,       $sale->units,
        $sale->price,           $sale->free ),
        "\n";

}

sub _findMatch
{
    my $self = shift;
    my %args = @_;

    my $saleDB = $args{sale};

    my $data = {
        service_id         => $self->{fileServiceID},
        media_type         => $saleDB->MediaType,
        album              => $saleDB->AlbumName,
        artist             => $saleDB->ArtistName,
        client_product_id  => $saleDB->ClientProductID,
        format             => $saleDB->FormatType,
        isrc               => $saleDB->ISRC,
        product_type       => $saleDB->ProductType,
        service_product_id => $saleDB->ServiceProductID,
        track_num          => $saleDB->TrackNum,
        track              => $saleDB->TrackName,
        upc                => $saleDB->UPC,
        outlet             => $saleDB->Outlet,
    };

    my $match = RPS::Sale::Match->new(client_id => $self->{clientID});
    my $result = $match->FindBestMatch(data => $data, min_match_level => 90, skip_rec => 1);

    $saleDB->ImportStatus($result->ImportStatus);
    if ($saleDB->ImportStatus == File::Sale::STATUS_MATCH)
    {
        $saleDB->ProductID($result->ProductIDs->[0]);
    }
    elsif ($saleDB->ImportStatus == File::Sale::STATUS_MAPPED)
    {
        $saleDB->ProductID($result->ProductIDs->[0]);
        $saleDB->MapID($result->MapID);
    }
    elsif ($saleDB->ImportStatus == File::Sale::STATUS_AUTO_MAPPED)
    {
        $saleDB->ProductID($result->ProductIDs->[0]);
        $saleDB->MapID($result->MapID);
    }
    elsif ($saleDB->ImportStatus == File::Sale::STATUS_BATCH_MAPPED)
    {
        $saleDB->ProductID($result->ProductIDs->[0]);
        $saleDB->MapID($result->MapID);
    }
    elsif ($saleDB->ImportStatus == File::Sale::STATUS_DONT_MATCH)
    {
        $saleDB->ProductID($result->ProductIDs->[0]);
        $saleDB->MapID($result->MapID);
    }
}

sub _getServiceMap
{
    $SERVICE_MAP = Client::Service::GetServiceNameMap( clean => 1 );

    # some "other" variations of service names we support
    $SERVICE_MAP->{'3gupload/mixxer_inc'} = Client::Service::DSP_MIXXER;
    $SERVICE_MAP->{'9_squared'}     = Client::Service::DSP_NINESQUARED;
    $SERVICE_MAP->{america_online}  = Client::Service::DSP_AOL;
    $SERVICE_MAP->{at_t_wireless_services} = Client::Service::DSP_ATT;
    $SERVICE_MAP->{docomo}          = Client::Service::DSP_NTTDOCOMO;
    $SERVICE_MAP->{groovemobile}    = Client::Service::DSP_GROOVEMOBILE;
    $SERVICE_MAP->{microsoft}       = Client::Service::DSP_MSNMUSIC;
    $SERVICE_MAP->{musicnet}        = Client::Service::DSP_MEDIANET;
    $SERVICE_MAP->{realnetworks}    = Client::Service::DSP_REAL;
    $SERVICE_MAP->{rhapsody}        = Client::Service::DSP_REAL;
    $SERVICE_MAP->{ringtonejukebox} = Client::Service::DSP_RINGTONEJUKEBOX;
    $SERVICE_MAP->{imeem}           = Client::Service::DSP_IMEEMINC;
    $SERVICE_MAP->{jriver}           = Client::Service::DSP_JRIVER;
    $SERVICE_MAP->{slacker}         = Client::Service::DSP_SLACKERINC;
    $SERVICE_MAP->{djtunes}         = Client::Service::DSP_DJTUNES;
    $SERVICE_MAP->{hip_digital_media_us} = Client::Service::DSP_HIP_DIGITAL_MEDIA;


    # add some known alternate naming conventions
    my %altNames = ();
    while (my ($svc, $id) = each %$SERVICE_MAP)
    {
        next unless ($svc =~ s/$SERVICE_DECORATION//);
        next if (exists $SERVICE_MAP->{$svc});

        if (exists $altNames{$svc})
        {
            delete $altNames{$svc};
            next;
        }
        $altNames{$svc} = $id;
    }

    while (my ($svc, $id) = each %altNames)
    {
        $SERVICE_MAP->{$svc} = $id;
    }
}

sub _checkForRevenueWithoutUnits {
    my ($self, $saleRec) = @_;

    # If we have a digital sale with revenue but no units, set units to 1 or -1.
    # Also, reset the price since unitPrice will have set it to 0.
    if ($self->{_file}->Physical == 0 && $saleRec->units == 0 && $saleRec->price)
    {
        my $price = $self->_formatPrice( $saleRec->price );
                
        if ($price < 0)
        {
            $saleRec->units(-1);
        }
        elsif ($price > 0)
        {
            $saleRec->units(1);
        }
        
        $saleRec->price( abs($price) )
    }
}

sub _sanitizeFormatType
{
    my ($self, $saleRec) = @_;
    
    if ($saleRec->price > 0)
    {
        # thankfully (at least i think so?), publishing royalties are paid directly
        # in other countries besides US and Canada, so not going to worry about
        # those. which is nice, because it's not a manageable thing to sanity check
        # prices in, well, every other currency in the world.
        if ($saleRec->currencyCode eq 'USD' || $saleRec->currencyCode eq 'CAD')
        {
            # we'll treat albums and tracks the same, meaning if an album
            # download or ringtone can be co-erced to a stream, it must be
            # be a single-track album, at least practical purposes.
            #
            # the only thing magical about 20- or 50-cents is that it's twice
            # the going mechanical rate at the moment. so really, meaning
            # there's room to pay it.            
            if ($saleRec->formatType eq RPS::File::Sale::FORMAT_DOWNLOAD        ||
                $saleRec->formatType eq RPS::File::Sale::FORMAT_DUALDOWNLOAD    ||
                $saleRec->formatType eq RPS::File::Sale::FORMAT_DOWNLOADPREMIUM ||
                $saleRec->formatType eq RPS::File::Sale::FORMAT_VPD )
            {
                if ($saleRec->productType eq RPS::File::Sale::TYPE_TRACK && $saleRec->price < .20)
                {
                    $saleRec->formatType(RPS::File::Sale::FORMAT_STREAM);
                }
                elsif ($saleRec->productType eq RPS::File::Sale::TYPE_ALBUM && $saleRec->price < 2)
                {
                    $saleRec->formatType(RPS::File::Sale::FORMAT_STREAM);
                }
            }
            elsif ($saleRec->formatType eq RPS::File::Sale::FORMAT_RINGTONE)
            {
                if ($saleRec->price < .50)
                {
                    $saleRec->formatType(RPS::File::Sale::FORMAT_STREAM);
                }
            }
        }
    }
}

# Although SaleRec is its own class, I'm _not_ going to put the validate logic in there.
# A case could be made that it belongs there, but our current model has the Importer
# class in charge of its own validation.
#
# In other words, if we want to mess around with how certain Importers validate, they just have
# to override their appropriate '_validate*' method.   If we move validation into the SaleRec class,
# then importers will need to create their own subclass of SaleRec to hold any custom validation.
#
# That's not a terrible model - The Importers already instantiate SaleRec themselves.  But, meh.
# I think it makes more sense to leave SaleRec as a relatively dumb container class, and make sure
# Importers do all the heavy lifting. - JPK
#
sub _validateSaleRec
{
    my ($self, $saleRec) = @_;


    my $productTypeStr = $self->_validateProductType($saleRec);

    if($productTypeStr eq "physical" || $self->{_file}->Physical == 1)
    {
        $self->_validateSalesReturns($saleRec);
        $self->_validatePriceLevel($saleRec);
        $self->_validateChannel($saleRec);
        $self->_validatePhysicalFormatType($saleRec);
    }
    elsif($productTypeStr eq "digital" || $self->{_file}->Physical == 0)
    {
        $self->_validatePrice($saleRec);
        $self->_validateUnits($saleRec);
        $self->_validateDigitalFormatType($saleRec);
    }
    else
    {
        #!!! I don't think we ever actually get here, since all files would have 1 or 0 for physical.
        #!!! So, I'm not going to update this section any further.
        
    	# Let's just clear out the format type field since we can't validate it.
    	my $formatType = $saleRec->formatType;
    	$self->handleError( "Unable to validate format type: $formatType", RPS::DB::Item::SaleImportError::kErrorFormatType), 
    	$saleRec->formatType('');

        # Just in case we screw something up in the future...
        #
		#$self->handleError( "product type $productTypeStr was not recognized!", #RPS::DB::Item::SaleImportError::kErrorNonqualifying );       
        #assert(0, "product type $productTypeStr was not recognized!");
    }

    $self->_validateMediaType($saleRec);
    $self->_validateDates($saleRec);
    $self->_validateFree($saleRec);
    $self->_validateCurrencyCode($saleRec);
    $self->_validateCountryCode($saleRec);
    $self->_validateUPC($saleRec);
    $self->_validateService($saleRec);
}

#------------------------------------------------------------
# str _validateProductType
#
# This subroutine takes in a sale record, and dies
# if the product type is undefined or invalid. If the product
# type is valid then a string indicating the category of the
# product type is returned (digital or physical).
#
# Parameters:
#   str $productType (required) - A string that is a possible
#   product type.
#
#
# Return:
# str $productTypeStr
#------------------------------------------------------------

sub _validateProductType
{
    my ($self, $saleRec) = @_;

    my %validProductTypes =
    (
        RPS::File::Sale::TYPE_ALBUM          => "digital",
        RPS::File::Sale::TYPE_TRACK          => "digital",
        RPS::File::Sale::TYPE_PHYSICAL       => "physical",
        RPS::File::Sale::TYPE_LP             => "physical",
        RPS::File::Sale::TYPE_LP5            => "physical",
        RPS::File::Sale::TYPE_CD             => "physical",
        RPS::File::Sale::TYPE_VHS            => "physical",
        RPS::File::Sale::TYPE_CASS           => "physical",
        RPS::File::Sale::TYPE_VCD            => "physical",
        RPS::File::Sale::TYPE_EP             => "physical",
        RPS::File::Sale::TYPE_MD             => "physical",
        RPS::File::Sale::TYPE_DVD            => "physical",
        RPS::File::Sale::TYPE_BLURAY         => "physical",
        RPS::File::Sale::TYPE_MASTER         => "physical",
        RPS::File::Sale::TYPE_CAS_SIN        => "physical",
        RPS::File::Sale::TYPE_CD_SIN         => "physical",
        RPS::File::Sale::TYPE_DVD_CD_SET     => "physical",
        RPS::File::Sale::TYPE_DBL_CD         => "physical",
        RPS::File::Sale::TYPE_LICENSE_INCOME => "physical",
    );


    my $productType = $saleRec->productType;

    if (!$productType)
    {
        $productType = $self->handleError( "Missing product type", RPS::DB::Item::SaleImportError::kErrorProductType );
        $saleRec->productType($productType); 
        return; 	
    }

    if (!$validProductTypes{$productType})
    {
        $productType = $self->handleError( "Invalid product type: $productType", RPS::DB::Item::SaleImportError::kErrorProductType );	
        $saleRec->productType($productType); 
        return; 			    	
    }

    return $validProductTypes{$productType};
}


sub _validateSalesReturns
{
    my ($self, $saleRec) = @_;

    my $sales = $saleRec->sales;
    my $salesRevenue = $saleRec->salesRevenue ? $saleRec->salesRevenue : 0;
    my $returns = $saleRec->returns;
    my $returnsRevenue = $saleRec->returnsRevenue ? $saleRec->returnsRevenue : 0;
    my $totalRevenue = $saleRec->totalRevenue;

    #Go through all 4 possibilities for sales and returns being either
    #set or not set.
    if(!($sales || $returns))
    {
		return $self->handleError( "No sales or returns set for physical sale", RPS::DB::Item::SaleImportError::kErrorNonqualifying );       	
    }

    if($sales && !$returns)
    {
        $self->_validateSales($saleRec);
        $self->_validateSalesRevenue($saleRec);
        $self->_validateTotalRevenue($saleRec);
        $self->_validateSaleTotalSum($saleRec);
    }

    if($sales && $returns)
    {
        $self->_validateSales($saleRec);
        $self->_validateSalesRevenue($saleRec);
        $self->_validateReturns($saleRec);
        $self->_validateReturnsRevenue($saleRec);
        $self->_validateTotalRevenue($saleRec);
    }

    if(!$sales && $returns){
        $self->_validateReturns($saleRec);
        $self->_validateReturnsRevenue($saleRec);
        $self->_validateTotalRevenue($saleRec);

        $self->_validateReturnTotalSum($saleRec);
    }

    $self->_validateTotalSum($saleRec);
}

sub _validateTotalSum {
    my ($self, $saleRec) = @_;

	my $salesRevenue   = $saleRec->salesRevenue ? $saleRec->salesRevenue : 0;
    my $returnsRevenue = $saleRec->returnsRevenue ? $saleRec->returnsRevenue : 0;
    my $totalRevenue   = $saleRec->totalRevenue;

	# No matter WHAT, I expect this to be correct.
    #
    my $expectedRevenue = $salesRevenue - $returnsRevenue;

    #First check to see if the total revenue is within a penny of the
    #expected revenue, then check to see if the sign of the total revenue
    #matches the sign of $salesRevenue - $returnsRevenue within a penny
    if(((abs($totalRevenue) - abs($expectedRevenue)) > .01)||
        (($salesRevenue > $returnsRevenue) && (($totalRevenue + .01) < 0))||
        (($salesRevenue < $returnsRevenue) && (($totalRevenue - .01) > 0)))
    {
		return $self->handleError( "Total revenue $totalRevenue does not equal sales revenue $salesRevenue minus returns revenue $returnsRevenue", RPS::DB::Item::SaleImportError::kErrorNonqualifying );     	
    }
}

sub _validateReturnTotalSum {
    my ($self, $saleRec) = @_;

    my $returnsRevenue = $saleRec->returnsRevenue ? $saleRec->returnsRevenue : 0;
    my $totalRevenue = $saleRec->totalRevenue;

    if(((abs($totalRevenue) - abs($returnsRevenue)) > .01) ||
       (($totalRevenue - .01) > 0))
    {
		return $self->handleError( 
			"Returns revenue $returnsRevenue negated does not equal total revenue $totalRevenue for sale with no sales",
			RPS::DB::Item::SaleImportError::kErrorNonqualifying, 
		);     	
    }
}

sub _validateSaleTotalSum {
    my ($self, $saleRec) = @_;

    my $salesRevenue = $saleRec->salesRevenue ? $saleRec->salesRevenue : 0;
    my $totalRevenue = $saleRec->totalRevenue;

    if(((abs($salesRevenue) - abs($totalRevenue)) > .01) ||
       (($totalRevenue + .01) < 0))
    {
		return $self->handleError( "Sales revenue $salesRevenue does not match total revenue $totalRevenue for sale with no returns", RPS::DB::Item::SaleImportError::kErrorNonqualifying );
    }
}

sub _validateSales
{
    my ($self, $saleRec) = @_;

    my $sales = $saleRec->sales;

    if (!defined $sales)
    {
		return $self->handleError( "Sales not defined for physical sale", RPS::DB::Item::SaleImportError::kErrorNonqualifying );       	
    }

    if($sales < 0)
    {
		return $self->handleError( "Sales $sales is invalid because it is negative", RPS::DB::Item::SaleImportError::kErrorNonqualifying );       	
    }

    if($sales !~ /^\d+$/)
    {
		return $self->handleError( "Sales $sales in non-positive integer format", RPS::DB::Item::SaleImportError::kErrorNonqualifying );   	
    }
}

#------------------------------------------------------------
# Sales revenue value must be in one of the following formats,
# with arbitrary number of digits:
# 00
# 00.
# 00.00
# .00
#------------------------------------------------------------
sub _validateSalesRevenue
{
    my ($self, $saleRec) = @_;

    my $salesRevenue = $saleRec->salesRevenue;

    if(!defined $salesRevenue)
    {
		return $self->handleError( "Sales revenue not defined", RPS::DB::Item::SaleImportError::kErrorNonqualifying ); 	
    }

    if($salesRevenue < 0)
    {
		return $self->handleError( "Sales revenue $salesRevenue is invalid because it is negative", RPS::DB::Item::SaleImportError::kErrorNonqualifying );     	
    }

    if($salesRevenue !~ /^(?:(?:\d+)?\.\d+|\d+(?:\.)?(?:\d+)?)$/)
    {
		return $self->handleError( "Sales revenue $salesRevenue is in an incorrect format", RPS::DB::Item::SaleImportError::kErrorNonqualifying );         	
    }
}

#------------------------------------------------------------
# Total revenue value must be in one of the following formats,
# with arbitrary number of digits:
# 00
# -00
# 00.
# -00.
# 00.00
# -00.00
# .00
# -.00
#------------------------------------------------------------
sub _validateTotalRevenue
{
    my ($self, $saleRec) = @_;
    my $totalRevenue = $saleRec->totalRevenue;

    if(!defined $totalRevenue)
    {
		return $self->handleError( "Total revenue not defined for physical sale", RPS::DB::Item::SaleImportError::kErrorNonqualifying );    	
    }

    if($totalRevenue !~ /^\-?(?:(?:\d+)?\.\d+|\d+(?:\.)?(?:\d+)?)$/)
    {
		return $self->handleError( "Total revenue $totalRevenue is in an incorrect format", RPS::DB::Item::SaleImportError::kErrorNonqualifying ); 	
    }
}

sub _validateReturns
{
    my ($self, $saleRec) = @_;
    my $returns = $saleRec->returns;

    if($returns < 0)
    {
		return $self->handleError( "Returns $returns is invalid because it is negative", RPS::DB::Item::SaleImportError::kErrorNonqualifying );        	
    }

    if ($returns !~ /^\d+$/)
    {
		return $self->handleError( "Returns $returns in non-positive integer format", RPS::DB::Item::SaleImportError::kErrorNonqualifying );      	
    }
}

sub _validateReturnsRevenue
{
    my ($self, $saleRec) = @_;

    my $returnsRevenue = $saleRec->returnsRevenue;

    if (!defined $returnsRevenue)
    {
		return $self->handleError( "Returns revenue has not been set for physical sale with returns", RPS::DB::Item::SaleImportError::kErrorNonqualifying );    	
    }

    if ($returnsRevenue < 0)
    {
		return $self->handleError( "Returns revenue $returnsRevenue is invalid because it is negative", RPS::DB::Item::SaleImportError::kErrorNonqualifying );        	
    }

    if ($returnsRevenue !~ /^(?:(?:\d+)?\.\d+|\d+(?:\.)?(?:\d+)?)$/)
    {
		return $self->handleError( "Returns revenue $returnsRevenue is in an incorrect format", RPS::DB::Item::SaleImportError::kErrorNonqualifying );  	
    }
}

sub _validatePriceLevel
{
    my ($self, $saleRec) = @_;
    my $priceLevel = $saleRec->priceLevel;

    my %priceLevels =
    (
        # Unknown _is_ a valid price level - It means we'll use the product's default.
        #
        RPS::File::Sale::PLEVEL_UNKNOWN => 1,
        RPS::File::Sale::PLEVEL_FULL => 1,
        RPS::File::Sale::PLEVEL_MID => 1,
        RPS::File::Sale::PLEVEL_BUDGET => 1,
        RPS::File::Sale::PLEVEL_PROMO => 1,
    );

    if(!defined $priceLevel)
    {
		return $self->handleError( "Price level for physical sale has not been set", RPS::DB::Item::SaleImportError::kErrorNonqualifying );     	
    }

    if(! $priceLevels{$priceLevel})
    {
		return $self->handleError( "Unknown price level $priceLevel", RPS::DB::Item::SaleImportError::kErrorNonqualifying );	
    }
}

sub _validateChannel
{
    my ($self, $saleRec) = @_;
    my $channel = $saleRec->channel;

    my %channels =
    (
        RPS::File::Sale::CHANNEL_RETAIL => 1,
        RPS::File::Sale::CHANNEL_MILITARY => 1,
        RPS::File::Sale::CHANNEL_CLUB => 1,
        RPS::File::Sale::CHANNEL_MAILORDER => 1,
        RPS::File::Sale::CHANNEL_DIRECT => 1,
    );

   if(!defined $channel)
   {
		return $self->handleError( "Channel for physical sale has not been set", RPS::DB::Item::SaleImportError::kErrorNonqualifying );     	
   }

   if (! $channels{$channel})
   {
		return $self->handleError( "Unknown channel $channel", RPS::DB::Item::SaleImportError::kErrorNonqualifying );  	
   }
}

sub _validatePrice
{
    my ($self, $saleRec) = @_;
    my $price = $saleRec->price;

    if(!defined $price)
    {
        #die "Price not defined for digital sale";
        # !!! Why would we not want to die?
        # !!! When is not having a price valid?
        #
        return;
    }

    if($price < 0)
    {
		return $self->handleError( "Price $price is invalid because it is negative", RPS::DB::Item::SaleImportError::kErrorNonqualifying );       	
    }

    # 0 | 0.0 | .00 |0.
    if($price !~ /^(?:(?:\d+)?\.\d+|\d+(?:\.)?(?:\d+)?)$/)
    {
		return $self->handleError( "Price $price is in an incorrect format", RPS::DB::Item::SaleImportError::kErrorNonqualifying );  	
    }
}

sub _validateUnits
{
    my ($self, $saleRec) = @_;
    my $units = $saleRec->units;

    if(!defined $units)
    {
		return $self->handleError( "Units not set for digital sale", RPS::DB::Item::SaleImportError::kErrorNonqualifying );      	
    }

    if($units !~ /^\-?\d+$/)
    {
		return $self->handleError( "Units $units in non-integer format", RPS::DB::Item::SaleImportError::kErrorNonqualifying );    	
    }
}

sub _validatePhysicalFormatType
{
    my ($self, $saleRec) = @_;

    my $formatType = $saleRec->formatType;

    # This should be easy - There should not BE a formatType declared.
    #
    if ($formatType)
    {
        my $productType = $saleRec->productType;
		return $self->handleError( "FormatType $formatType specified for a physical sale (with product type $productType)", RPS::DB::Item::SaleImportError::kErrorNonqualifying );          
    }
}

sub _validateDigitalFormatType
{
    my ($self, $saleRec) = @_;

    my $formatType = $saleRec->formatType;  

    # Note: the following format types have been removed (FB1324)
    #
    #  FORMAT_MECHANICAL
    #  FORMAT_VIDEO
    #  FORMAT_VIDEOSTREAM
    #  FORMAT_RADIO
    #  FORMAT_TELEVISION
    #  FORMAT_MASTERTONE
    #  FORMAT_ANIMATEDRINGTONE
    #  FORMAT_VOICERINGER
    #  FORMAT_VIDEORINGER
    #  FORMAT_MIDI
    #  FORMAT_GRAPHIC
    #  FORMAT_SMSTONE
    #
    my %formatTypes =
    (RPS::File::Sale::FORMAT_DOWNLOAD => 1,
    RPS::File::Sale::FORMAT_DOWNLOADPREMIUM =>1,
    RPS::File::Sale::FORMAT_DOWNLOADUPGRADE => 1,
    RPS::File::Sale::FORMAT_STREAM => 1,
    RPS::File::Sale::FORMAT_TETHERED => 1,
    RPS::File::Sale::FORMAT_RINGTONE => 1,
    RPS::File::Sale::FORMAT_JUKEBOX => 1,
    RPS::File::Sale::FORMAT_DUALDOWNLOAD => 1,
    RPS::File::Sale::FORMAT_BACKGROUNDMUSIC => 1,
    RPS::File::Sale::FORMAT_VPD => 1,

    # wireless format types
    RPS::File::Sale::FORMAT_RINGBACK => 1,

    # other
    RPS::File::Sale::FORMAT_PERFORMANCE => 1,
    );

    if(!defined $formatType)
    {
		$formatType = $self->handleError( "No format type set for digital sale", RPS::DB::Item::SaleImportError::kErrorFormatType );  
		$saleRec->formatType($formatType);
		return;	
    }

    if(!$formatTypes{$formatType})
    {
		$formatType = $self->handleError( "Invalid or unsupported format type: $formatType", RPS::DB::Item::SaleImportError::kErrorFormatType );	   
		$saleRec->formatType($formatType);
		return;			 	
    }

    # !!! It would be a good idea to make sure the format type matches up with the product type in a
    # sane fashion...
    #
}


sub _validateMediaType
{
    my ($self, $saleRec) = @_;

    my $mediaType = $saleRec->mediaType;

    my %validMediaTypes =
    (RPS::DB::Item::MediaType::kMediaTypeAudio => "audio",
     RPS::DB::Item::MediaType::kMediaTypeVideo => "video",);

    if(!$mediaType)
    {
		$mediaType =  $self->handleError( "Media type not set for sale", RPS::DB::Item::SaleImportError::kErrorMediaType );	
		$saleRec->mediaType($mediaType);
		return;
    }

    if(!$validMediaTypes{$mediaType})
    {
		$mediaType = $self->handleError( "Invalid media type: $mediaType", RPS::DB::Item::SaleImportError::kErrorMediaType ); 	   
		$saleRec->mediaType($mediaType);
		return;		 	
    }

}

sub _validateDates
{
    my ($self, $saleRec) = @_;

    my $dateBegin = $saleRec->dateBegin;
    my $dateEnd   = $saleRec->dateEnd;

    #Individual year, month, day values of $dateBegin
    my $beginYear;
    my $beginMonth;
    my $beginDay;

    #Individual year, month, day values of $dateEnd
    my $endYear;
    my $endMonth;
    my $endDay;

    #number of days in the month of $dateEnd in the year of $dateEnd
    my $daysInMonth;

    #dateBegin and dateEnd converted into Unix time to do comparison
    my $dateBeginUnix;
    my $dateEndUnix;

    #Current Unix time
    my $currentTime = time();

    #Get the current year to check year ranges
    my ($sec,$min,$hour,$mday,$mon,$currentYear,$wday,$yday,$isdst) =
    localtime($currentTime);

    #Must add 1900 to current year to get 4 digit year value
    $currentYear += 1900;


    #Die if $dateBegin or $dateEnd is missing
    if(!$dateBegin)
    {
		return $self->handleError( "No date begin", RPS::DB::Item::SaleImportError::kErrorNonqualifying );   	
    }

    if(!$dateEnd)
    {
		return $self->handleError( "No date end", RPS::DB::Item::SaleImportError::kErrorNonqualifying );  	
    }

    #Make sure $dateBegin and $dateEnd are in valid ISO format (yyyy-mm-dd)
    if($dateBegin =~ /^(\d{4})\-(\d{2})\-(\d{2})$/)
    {
        $beginYear = $1;
        $beginMonth = $2;
        $beginDay = $3;
    }
    else
    {
		return $self->handleError( 
			"'Date begin' in invalid format $dateBegin, need: yyyy-mm-dd", 
			RPS::DB::Item::SaleImportError::kErrorNonqualifying, 
		);   	
    }

    if($dateEnd =~ /^(\d{4})\-(\d{2})\-(\d{2})$/)
    {
        $endYear = $1;
        $endMonth = $2;
        $endDay = $3;
    }
    else
    {
		return $self->handleError( 
			"'Date end' in invalid format $dateEnd, need: yyyy-mm-dd",
			RPS::DB::Item::SaleImportError::kErrorNonqualifying, 
		);      	
    }

    #Check $dateBegin date ranges
    if(($beginYear < 1990) || ($beginYear > $currentYear)){
		return $self->handleError( "Date begin year $beginYear out of range", RPS::DB::Item::SaleImportError::kErrorNonqualifying );    	
    }

    if(($beginMonth == 0) || ($beginMonth > 12)){
		return $self->handleError( "Date begin month $beginMonth out of range", RPS::DB::Item::SaleImportError::kErrorNonqualifying );      	
    }

    if($beginDay != 1){
		return $self->handleError( "Date begin day $beginDay not equal to 1", RPS::DB::Item::SaleImportError::kErrorNonqualifying );   	
    }

    #check $dateEnd date ranges
    if(($endYear < 1990) || ($endYear > ($currentYear + 1))){
		return $self->handleError( "Date end year $endYear out of range", RPS::DB::Item::SaleImportError::kErrorNonqualifying );  	    	
    }

    if(($endMonth == 0) || ($endMonth > 12)){
		return $self->handleError( "Date end month $endMonth out of range", RPS::DB::Item::SaleImportError::kErrorNonqualifying );    	
    }

    $daysInMonth = Days_in_Month($endYear, $endMonth);

    if($endDay != $daysInMonth){
		return $self->handleError( "Date end day $endDay is not last day in end month $endMonth in end year $endYear", RPS::DB::Item::SaleImportError::kErrorNonqualifying );    	
    }

    #Make sure $dateBegin is earlier than $dateEnd
    $dateBeginUnix = timegm(0,0,0,$beginDay,$beginMonth - 1,$beginYear);

    $dateEndUnix = timegm(0,0,0,$endDay,$endMonth - 1,$endYear);

    if($dateBeginUnix >= $dateEndUnix){
		return $self->handleError( "Date begin $dateBegin is equal to or later than date end $dateEnd", RPS::DB::Item::SaleImportError::kErrorNonqualifying );    	
    }
}

sub _validateFree
{
    my ($self, $saleRec) = @_;

    my $free = $saleRec->free;

    # Free isn't always defined.
    #
    return unless defined $free;

    if($free == 0 || $free == 1){
        return;
    }
    else{
		return $self->handleError( "Free must be 0 or 1 ('$free' is not valid)", RPS::DB::Item::SaleImportError::kErrorNonqualifying );   	
    }
}

sub _validateCurrencyCode
{
    my ($self, $saleRec) = @_;

    my $currencyCode = $saleRec->currencyCode;

     if(!defined $currencyCode)
     {
		return $self->handleError( "Currency code not set", RPS::DB::Item::SaleImportError::kErrorNonqualifying );
     }

    if (! Common::CurrencyFormat::CodeIsValid($currencyCode))
    {
		return $self->handleError( "Invalid or unsupported currency code: $currencyCode", RPS::DB::Item::SaleImportError::kErrorNonqualifying );    	
    }

}

sub _validateCountryCode
{
    my ($self, $saleRec) = @_;
    my $countryCode = $saleRec->countryCode;
    
	if( !$countryCode || $countryCode eq '') {
		return $self->handleError( "Missing country code", RPS::DB::Item::SaleImportError::kErrorNonqualifying );
	}

    # If we find a valid 2 or 3 character country code, save it back into the salerec.
    # This takes care of any lowercase country codes that verify OK but cause run-related
    # issues later on (see FB16152).
    #
    my $country = Common::Country->GetByAlpha2( $countryCode );
    if( !$country )
    {
        $country = Common::Country->GetByAlpha3( $countryCode );
    }
    $saleRec->countryCode($country->alpha2) if( $country );

    unless( $country )
    {
		$countryCode = $self->handleError( "Invalid or unsupported country code: $countryCode", RPS::DB::Item::SaleImportError::kErrorCountry );
		
		# We should get back either nothing (for non-mapface importers and mapface importers without a mapping)
		# or a country code that this test is mapped to.
		
		$saleRec->countryCode($countryCode);
		    		
		return;        
    }
}

sub _validateUPC
{
    my ($self, $saleRec) = @_;

    my $upc = $saleRec->upc;

    # Not all sales will have a UPC...
    #
    if ($upc)
    {
        # ... but if they do, it should not be gibberish.
        #
        # The Validate::Util::is_valid_upc_or_ean method is TOO strict.
        # We're going to get weird UPCs - All we really want is to insure
        # that these _look_ like UPCs.
        # Meaning:
        # - They are 12 or 13 characters in length.
        # - They only contain digits.
        # - ... and maybe that the last 4 or 5 digits are not all 0s (which
        #       is usually a signal that they were truncated and re-expanded).
        #
        # JPK - Well, unfortunately, we need to accept extremely poor 'UPC' values.
        # At this point, I'm just going to complain if they give us any non-digits.
        #
#        if (! Validate::Util::is_valid_upc_or_ean($upc))

#        if (((length($upc) != 12 ) && (length($upc) != 13))
#         || ($upc =~ /\D/)
#         || (substr($upc, -5) eq '00000'))
#        if ($upc =~ /\D/)
#        {
#            die Import::ValidationError->new($saleRec, "Invalid UPC '$upc'");
#        }
    }
}

sub _validateService
{
    my ($self, $saleRec) = @_;

    my $serviceID = $saleRec->serviceID;

    if ($serviceID)
    {
        my $formatType = $saleRec->formatType;  
        if ($serviceID == Client::Service::DSP_SOUNDEXCHANGE && $formatType ne RPS::File::Sale::FORMAT_PERFORMANCE) {
            return $self->handleError( "SoundExchange sales must have a format type of Performance Income", RPS::DB::Item::SaleImportError::kErrorNonqualifying );
        }
    }
}

sub unitPrice {
    my $self = shift;
	my $netPrice = $self->_formatPrice( $self->price() ) || 0;
    my $unitPrice = ($self->units()) ?  $netPrice / $self->units() : $netPrice;
    
    # Negative zero?  Really?
    # I am disappoint.
    if ($unitPrice eq "-0") {
        $unitPrice = 0;
    }

    return $unitPrice;
}

sub _saveLine {
    my $self = shift;

    my ($startDate, $endDate) = $self->saleDates();

	my $unitPrice = $self->unitPrice;

    my $sale = RPS::Import::SaleRec->new();

    $sale->price( $unitPrice );

    $sale->dateBegin($startDate);
    $sale->dateEnd($endDate);

	$sale->mediaType(           $self->mediaType);
    $sale->productType(         $self->productType);
    $sale->formatType(          $self->formatType);
    $sale->albumName(           $self->albumName );
    $sale->artistName(          $self->artistName );
    $sale->averagePrice(        $self->averagePrice );
    $sale->channel(             $self->channel );
    $sale->clientProductID(     $self->clientProductID );
    $sale->configuration(       $self->configuration );
    $sale->conversionRate(      $self->conversionRate );
    $sale->countryCode(         $self->countryCode );
    $sale->currencyCode(        $self->currencyCode );
    $sale->free(                $self->free );
    $sale->importStatus(        $self->importStatus );
    $sale->isrc(                $self->isrc );
    $sale->labelName(           $self->labelName );
    $sale->outlet(              $self->outlet );
    $sale->payoutType(          $self->payoutType );
    $sale->wholesalePrice(      $self->wholesalePrice );
    $sale->retailPrice(         $self->retailPrice );
    $sale->priceLevel(          $self->priceLevel );
    $sale->priceType(           $self->priceType );
    $sale->retail(              $self->retail );
    $sale->returns(             $self->returns );
    $sale->returnsRevenue(      $self->returnsRevenue );
    $sale->sales(               $self->sales );
    $sale->salesRevenue(        $self->salesRevenue );
    $sale->serviceID(           $self->serviceID );
    $sale->serviceProductID(    $self->serviceProductID );
    $sale->totalRevenue(        $self->totalRevenue );
    $sale->trackName(           $self->trackName );
    $sale->trackNum(            $self->trackNum );
    $sale->units(               $self->units );
    $sale->upc(                 $self->upc );
    $sale->wholesaleRate(       $self->wholesaleRate );
    $sale->mcps_adjustment(     $self->mcps_adjustment );
    $sale->comments(            $self->comments );

	$sale->lineNum( $self->linenum );

    $self->_insertSale(sale => $sale);
}

sub _isValidSaleRecord {
	my $self = shift;

	return ( defined($self->units) && ($self->trackName || $self->albumName) );
}

sub handleError {
	my ($self, $description, $type) = @_;
	$self->fail($description);
}


###
1;# Play nicely.
###
