package Support::Implementation::ArtistContractTemplate;
# 4/7/10 - Get price information from RSCOMMON.price_level
# 4/17/10 - Added Retail/Document and Wholesale/Document support.
# - Added LP5 support
# 5/4/10 - Disallow DA/DT sources with retail/document and
#   wholesale/document (see _isValidContractRateType)
# - Check for duplicate payee exceptions.
# 5/5/10 - Added additional exception checking for channel
#   and price level with digital sources.
#   - Added support for "rs-payee-id"
# 5/7/10 - Removed trailing spaces from payee names when
#   reading them from DB
# - Allow leading spaces in payee name for MOS
# - Fixed clientID initialization
# 5/11/10 - Debugging why schedules were being dropped..
# - Added LP5 to _isPhysical. Note: _isPhysical is OBE.
# - Added logic to detect if contract already exists
# 7/7/10 - Updated to use contract_term_source_id vs.
#   income_source_id for contract terms.  Changed to use
#   RSCOMMON.contract_rate_type for rate types.
# 7/16/10 - Changes per FB 12125
# 8/25/10 - Removed trailing spaces from contract name
# 8/31/10 - Use rsPayeeID (if available) for contract key
# 10/4/10 - Allow percentage of sales to be set for all formats
# 1/5/11 - Added aliases for "ALL-D" and "ALL-P"
# 1/13/11 - Use client account ID when checking for duplicate
# payees.
# 2/18/11 - Updated contract rate type hash to allow UI-style types
# 2/23/11 - Added alias for 'mail-order' channel
# 3/15/11 - Added 'kludge' for VP to allow payee matches on payees
#   with funky characters..
# 8/31/11 - Enabled DT/DA_isValidContractRateType {
# 9/6/11 - Corrected duplicate payee handling
# 11/9/11 - Generate exception if invalid rs payee ID specified
# 1/17/12 - Added normalized date handling
# 5/1/12 - Changed to perform name comparisons using 'clean' names
# 8/12/13 - Added Fixed Rate synonym
# 12/4/13 - Removed newline from payor name
# 2/6/14 - Added Excel 2007 Support
# 3/10/14 - Added fixed price alias
# 10/31/14 - Generate warning (instead of crashing) for invalid payeeID format
#         - Added 'non payable' rate type alias
# 2/15/15 - Set contract_rate_type_id for nonpayable terms with an 'all' income source
# 4/3/15 - Updated _isPriceLevelAllowed w/ LP5, CAS5 and BluRay
# 6/24/15 - Added ALLD and ALLP aliases for income and contract term sources.
# 3/16/16 - Trim spaces off payor name
# 8/18/16 - Allow zero rate net revenue terms (FB14987).  Remove leading/trailing
#   spaces on artistName.
# 11/7/16 - Updated _isPriceLevelAllowed and _isContractRateTypeAlllowd to allow 'all'
# 11/16/16 - Allow income and contract term sources with or without hyphens
# 4/19/17 - Added PI checks for channel and price tier.  Added rate type aliases.
# 9/19/17 - Code clean-up; removed 'all' region default
# 12/12/18 - Log invalid rate type as an exception instead of dying; added 'all performance'
#  as an alias for 'PI'
#
#
use strict;
use warnings;

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

use IO::File;
use Data::Dumper;
use Date::Calc;

use Spreadsheet::ParseExcel;

use Common::Util qw(clean trimspaces);

use Common::Assert;
use Common::UTF8;
use Common::RSApp;

use RPS::DB::Item::ClientOptions;

use RPS::DB::Item::NewArtistContract;
use RPS::DB::Item::NewArtistContractTerm;
use RPS::DB::Item::IncomeSource;
use RPS::DB::Item::ContractRateType;
use RPS::DB::Item::ReserveLiquidation;

use RPS::DB::Item::Album;
use RPS::DB::Item::Track;

use RPS::DB::Item::ArtistPayee;
use RPS::DB::Item::Payor;
use RPS::DB::Item::Region;

use Support::Implementation::ImplementationUtil qw( report appendString checkMissingColumn printNull date2MySQL);

use Support::Implementation::ExcelReader;
use Support::Implementation::Excel2007Reader;
use Support::Implementation::TabDelimitedReader;
use base 'Support::Implementation::Template';

use constant kQuiet  => 0;
use constant kNormal => 1;
use constant kDebug  => 3;
my $gReportLevel = kNormal;

use constant kDuplicateTerm       => 1;
use constant kDefaultTermNotLast  => 2;

binmode STDOUT, ":utf8";


#use constant kClassAttributes => qw( name exec_mode client_id );

#-----------------------------------------------------------------------
# templateHeader maps column names to their default column number.
# The actual column number-header name map is stored in columnMap.
# Note(s):
# 1) Column names are case sensitive
# 2) Spaces are ignored; you must manually remove spaces from the names
#    stored in templateHeader.
# 
# TODO TODO TODO TODO TODO TODO TODO TODO TODO TODO TODO TODO TODO TODO
#
# Should modify logic to reject template if the column names don't
# match _exactly_.  See line 78 of ExcelReader.pm (scanExcelFile); this
# is where spaces are removed from the column name.
#-----------------------------------------------------------------------
my %gTemplateHeader = (
   "payee-client-account#"  => 0, # A
   "payee-name"             => 1, # A
   "contract-name"          => 2, # B
   "artist-name"            => 3, # C
   "payor-name"             => 4, # D
   "issue-date"             => 5, # E
   "contract-id"            => 6, # F
   "term-start"             => 7, # F
   "term-end"               => 8, # F
   "reserve-percentage"     => 9, # G
   "p1"                     => 10, # H
   "p2"                     => 11, # I
   "p3"                     => 12, # J
   "p4"                     => 13, # K
   "p5"                     => 14, # L
   "p6"                     => 15, # M
   "p7"                     => 16, # N
   "p8"                     => 17, # O
#   "Digital Reserves Taken" => 18, # O
   "default-term"           => 19, # P
   "income-source"          => 20, # Q
   "region"                 => 21, # R
   "channel"                => 22, # S
   "price-tier"             => 23, # T
   "rate-type"              => 24, # U
   "rate"                   => 25, # V
   "rate-reduction"         => 26, # W
   "percent-of-sales"       => 27, # X
   "packaging"              => 28, # Y
   "free-goods"             => 29, # Z
);

#--------------------------------------------------------------
# Maps column #'s to a unique key (headername).  The reverse of
# templateHeader, but with actual column #.
#--------------------------------------------------------------
my %gColumnMap = ();
sub parseHeader {
   my $self = shift;
}

#my $app; # app singleton
my $dbo;
my $dbh;
my $cdbo;
my $clientID;
my $execMode;

my %gRegionMap;


# gIncomeSourceMap is used for validation of terms; before the introduction
# of contract_term_source it was the only way of defining a term's source.
# What we'll do now is that we'll still use gIncomeSourceMap for validation
# (we'll just add a few more entries for the "All Digital", "All Physical"
# and "All" sources), but we'll also setup the contract_term_source field
# when constructing the contract term. -ES 7/7/10
my %gIncomeSourceMap;
my %gContractTermSourceMap;

my %gChannelMap;
my %gPriceLevelMap;
my %gContractRateTypeMap;
my %gPayeeIDMap; # map of payee IDs to their names
my %gPayeeCountMap; # of payees with a given name
my %gPayeeAccountCountMap; # of payees with a given name _and_ clientAccountID

sub new {
   my ( $class, %args ) = @_;
   my $self = bless {}, $class;

   $self->{dbo} = $dbo;

   return $self->_init(%args);
}

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

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

   $execMode = $self->exec_mode if ( $self->exec_mode );

   #my $clientID = $self->client_id;
   $clientID = $self->client_id; # 5/7/10

   my $app = Common::RSApp->new( clientID => $clientID );

   $dbo = Common::RSApp::GetClientDB();
   $dbh = $dbo->DBH;

   $cdbo = Common::RSApp::GetCommonDB();

   #------------------------------
   # Build a list of valid regions
   #------------------------------
   my $sql = "SELECT region_id, name FROM region";
   my $sth = $dbo->DoCmd($sql);
   while( my($id,$name) = $sth->fetchrow_array() ) {
      $gRegionMap{ lc $name } = $id;
   }
   # 9/19/17 - Don't default 'all' to regionID 0
   #$gRegionMap{ all } = 0; # Rest of World  4/17/10

   #-------------------------------------
   # Build a list of valid income sources
   #-------------------------------------
   $sql = "SELECT income_source_id, format, name FROM income_source";
   $sth = $cdbo->DoCmd($sql);
   while( my($id,$format,$name) = $sth->fetchrow_array() ) {
      my $val = join("\t",$format,$id);
      $gIncomeSourceMap{ lc $name } = $val;
      if( $name =~ /\-/ ) {
         my $_name = lc $name;
	 $_name =~ s/-//g;
         $gIncomeSourceMap{ $_name } = $val; # store a non-hyphenated alias
      }
      if ( $name eq 'PI' ) {
         $gIncomeSourceMap{ 'all performance' } = $val; # store alias for PI income source
      }
   }

   # The "all" sources will translate into income_source_id = NULL when stored
   # in the database, but in order to process the template we need a way to
   # identify the source.

   $gIncomeSourceMap{ 'all' }          = join("\t", 0, 0);   # 0 is for importer-use ONLY
   $gIncomeSourceMap{ 'all digital' }  = join("\t", 2, 254); # 254 is for importer-use ONLY
   $gIncomeSourceMap{ 'all-d' }        = join("\t", 2, 254); # 254 is for importer-use ONLY
   $gIncomeSourceMap{ 'alld' }         = join("\t", 2, 254); # 254 is for importer-use ONLY
   $gIncomeSourceMap{ 'all physical' } = join("\t", 1, 255); # 255 is for importer-use ONLY
   $gIncomeSourceMap{ 'all-p' }        = join("\t", 1, 255); # 255 is for importer-use ONLY
   $gIncomeSourceMap{ 'allp' }         = join("\t", 1, 255); # 255 is for importer-use ONLY
   $gIncomeSourceMap{ 'orchard sync' } = join("\t", 2, 30);


   #--------------------------------------------
   # Build a list of valid contract term sources
   #--------------------------------------------
   $sql = "SELECT contract_term_source_id, name, parent_contract_term_source_id "
      . "FROM contract_term_source";
   $sth = $cdbo->DoCmd($sql);
   while( my($id, $name, $parentID) = $sth->fetchrow_array() ) {
      $parentID = 0 if ( !$parentID );
      my $val = join("\t", $id, $parentID );
      $gContractTermSourceMap{ lc $name } = $val;
      if( $name =~ /\-/ ) {
         my $_name = lc $name;
	 $_name =~ s/-//g;
print STDERR "D: map: id($id) name($name) _name($_name)\n"; # XXX
         $gContractTermSourceMap{ $_name } = $val; # store a non-hyphenated alias
      }
   }
   # Add aliases for 'all digital' and 'all physical' sources
   $gContractTermSourceMap{ 'all physical'} = join("\t", 2, 1);
   $gContractTermSourceMap{ 'all-p'} = join("\t", 2, 1);
   $gContractTermSourceMap{ 'allp'}  = join("\t", 2, 1);
   $gContractTermSourceMap{ 'all digital'} = join("\t", 3, 1);
   $gContractTermSourceMap{ 'all-d'} = join("\t", 3, 1);
   $gContractTermSourceMap{ 'alld'}  = join("\t", 3, 1);

   #-------------------------------
   # Build a list of valid channels
   #-------------------------------
   $sql = "SELECT channel_id, name FROM channel";
   $sth = $dbo->DoCmd($sql);
   while( my($id,$name) = $sth->fetchrow_array() ) {
      $gChannelMap{ lc $name } = $id;

      $gChannelMap{ 'mail-order'} = $id if ( (lc $name) eq 'mail order' );
   }
   $gChannelMap{all} = 0;

   #-----------------------------------
   # Build a list of valid price levels
   #-----------------------------------
   $sql = "SELECT price_level_id, name FROM price_level";
   $sth = $cdbo->DoCmd($sql);
   while( my($id,$name) = $sth->fetchrow_array() ) {
      $gPriceLevelMap{ lc $name } = $id;
      $gPriceLevelMap{album} = $id if ( $name =~ m/^album/ );
      $gPriceLevelMap{track} = $id if ( $name =~ m/^track/ );
   }
   $gPriceLevelMap{all} = 0;

   #------------------------------------------
   # Build a list of valid contract rate types
   #------------------------------------------
   $sql = "SELECT contract_rate_type_id, name FROM contract_rate_type";
   $sth = $cdbo->DoCmd($sql); # 7/7/10
   while( my($id,$name) = $sth->fetchrow_array() ) {
      $gContractRateTypeMap{ lc $name } = $id;

      # store synonyms that are used by the template specification 1.1
      # Note: keys should have no spaces. -ES 10/31/14
      $gContractRateTypeMap{ retail }    = $id if ( $name =~ m/retail$/i );
      $gContractRateTypeMap{ '%retail' } = $id if ( $name =~ m/retail$/i );

      $gContractRateTypeMap{ wholesale }    = $id if ( $name =~ m/wholesale$/i );
      $gContractRateTypeMap{ '%wholesale' } = $id if ( $name =~ m/wholesale$/i );

      $gContractRateTypeMap{ ppd }       = $id if ( $name =~ m/ppd$/i );

      $gContractRateTypeMap{ "net revenue" } = $id if ( $name =~ m/net revenue/i );
      $gContractRateTypeMap{ "netrevenue" }  = $id if ( $name =~ m/net revenue/i );
      $gContractRateTypeMap{ '%netrevenue' } = $id if ( $name =~ m/net revenue/i );

      $gContractRateTypeMap{ "gross revenue" } = $id if ( $name =~ m/gross revenue/i );
      $gContractRateTypeMap{ "grossrevenue" }  = $id if ( $name =~ m/gross revenue/i );
      $gContractRateTypeMap{ '%grossrevenue' } = $id if ( $name =~ m/gross revenue/i );

      $gContractRateTypeMap{ fixed } = $id if ( $name =~ m/fixed/i );
      $gContractRateTypeMap{ fixedrate } = $id if ( $name =~ m/fixed/i ); # seen w/ FB627
      $gContractRateTypeMap{ 'fixedprice' } = $id if ( $name =~ m/fixed/i ); # seen w/ MOS (FB3695, et al)

      $gContractRateTypeMap{ 'retail-document' } = $id if ( $name =~ m/% Retail Document$/i ); # NEW - 4/17/10 <-- official
      $gContractRateTypeMap{ 'retaildocument' }  = $id if ( $name =~ m/% Retail Document$/i ); # NEW - 7/20/10
      $gContractRateTypeMap{ '%retaildocument' } = $id if ( $name =~ m/% Retail Document$/i ); # NEW - 7/20/10
      $gContractRateTypeMap{ 'retail-document' } = $id if ( $name =~ m/% Retail - Document$/i ); # NEW - 4/17/10

      $gContractRateTypeMap{ 'wholesale-document' } = $id if ( $name =~ m/\% Wholesale Document$/i ); # NEW - 4/17/10 <-- official
      $gContractRateTypeMap{ 'wholesaledocument' }  = $id if ( $name =~ m/\% Wholesale Document$/i ); # NEW - 7/20/10
      $gContractRateTypeMap{ '%wholesaledocument' } = $id if ( $name =~ m/\% Wholesale Document$/i ); # NEW - 7/20/10
      $gContractRateTypeMap{ 'wholesale-document' } = $id if ( $name =~ m/\% Wholesale - Document$/i ); # NEW - 4/17/10

      $gContractRateTypeMap{ 'nonpayable' } = $id if ( $name =~ /non-payable/i );

      $gContractRateTypeMap{ '% retail document' } = $id if ( $name =~ /retail document$/i ); # 4/19/17
      $gContractRateTypeMap{ '% wholesale document' } = $id if ( $name =~ /wholesale document$/i ); # 4/19/17
      $gContractRateTypeMap{ '% retail' } = $id if ( $name =~ /retail$/i ); # 4/19/17
      $gContractRateTypeMap{ '% wholesale' } = $id if ( $name =~ /wholesale$/i ); # 4/19/17
      $gContractRateTypeMap{ '% net revenue' } = $id if ( $name =~ /netrevenue$/i ); # 4/19/17
      $gContractRateTypeMap{ '% gross revenue' } = $id if ( $name =~ /grossrevenue$/i );
      $gContractRateTypeMap{ 'non payable' } = $id if ( $name =~ /^non-payable$/i ); # 4/19/17
      $gContractRateTypeMap{ 'nonpayable' } = $id if ( $name =~ /^non-payable$/i ); # 4/19/
   }

   #----------------------------------
   # Build a list of payee information
   #----------------------------------
   $sql = "SELECT artist_payee_id, name, client_account_id FROM artist_payee";
   $sth = $dbo->DoCmd($sql);
   while( my($id,$name, $clientAccountID) = $sth->fetchrow_array() ) {
      $name            =~ s/\s*$//g; # 5/7/10
      $name            = lc $name; # 9/6/11
      $clientAccountID =~ s/\s*$//g if ( $clientAccountID );
      $gPayeeIDMap{$id} = $name;

      $clientAccountID = '' if ( !$clientAccountID );
      #++$gPayeeCountMap{ join("\t",$name, $clientAccountID) };

      ++$gPayeeCountMap{ $name };
      ++$gPayeeAccountCountMap{ join("\t",$name, $clientAccountID) };
   }

   return $self;
}

sub getHeader {
   my $self = shift;
   #shift->{gTemplateHeader};
   return %gTemplateHeader;
}

sub getColumnMap {
   my $self = shift;
   return %gColumnMap;
}

sub getDbo {
   my $self = shift;
   return $dbo;
}

#--------------------------------------------------------------
# Read-in all of the template data into memory.  Once it's been
# read, try to parse it and
#--------------------------------------------------------------

my $reader;

sub loadMemory {
   my $self = shift;

   my $clientID = $self->client_id;
   my $app = Common::RSApp->new( clientID => $clientID );
#
   $dbo = Common::RSApp::GetClientDB();
   $dbh = $dbo->DBH;
#
   $cdbo = Common::RSApp::GetCommonDB();

   my $fileName = $self->name;


   #-----------------------------------------------------------
   # Check if an artist run is currently queued or running.  If
   # so, abort the import.
   #-----------------------------------------------------------
   my $sql = "SELECT DISTINCT status FROM artist_royalty_run ";
   my $sth = $dbo->DoCmd($sql);
   while( my($status) = $sth->fetchrow_array() ) {
      die("ARTIST RUN IN PROGRESS - ABORTING IMPORT")       if ( $status == 0 );
      die("ARTIST RUN WAITING TO COMMIT - ABORTING IMPORT") if ( $status == 7 );
      die("ARTIST RUN IS QUEUED - ABORTING IMPORT")         if ( $status == 6 );
   }


   if( $self->isExcel2003( $fileName ) || $self->isExcel2007( $fileName ) ) {

      my %data;
      if( $self->isExcel2003( $fileName ) ) {
         print("ArtistContractTemplate::loadMemory -- loading Excel2k3 $fileName into memory\n");

         #--------------------------
         # Read in the 1st worksheet
         #--------------------------
         $reader = Support::Implementation::ExcelReader->new(
            filename => $fileName,
            data => \%data,
            header => \%gTemplateHeader,
            columnmap => \%gColumnMap,
            tab => 1,
         );
      } else {
         print("ArtistContractTemplate::loadMemory -- loading Excel2k7 $fileName into memory\n");

         #--------------------------
         # Read in the 1st worksheet
         #--------------------------
         $reader = Support::Implementation::Excel2007Reader->new(
            filename => $fileName,
            data => \%data,
            header => \%gTemplateHeader,
            columnmap => \%gColumnMap,
            tab => 1,
         );

      }
      $reader->scanExcelFile();

      #----------------------------------------------------------------------------
      # Print the report header
      #
      # TODO: This should be dynamically built using the gColumnMap hash, but
      # then I'd have to rework the "STATUS" rows in the _validate routine so
      # that the values lined up under the correct column.  Wait.. scratch
      # that, I've have to modify the contractBuffer hash to store each
      # template line as a hash of key-value pairs (currently it just stores
      # each template line as a tab-delimited string).  Anyway, we'll save
      # this for the next rev...
      # The columns here must match the data output via _printReportLine
      #----------------------------------------------------------------------------
      report(join("\t", "STATUS:",
         "payee-client-account#",
         "payee-name",
         "rs-payee-id",
         "contract-name",
         "artist-name",
         "payor-name",
         "issue-date",
         "contract-id",
         "term-start",
         "term-end",
         "reserve-percentage",
         "p1",
         "p2",
         "p3",
         "p4",
         "p5",
         "p6",
         "p7",
         "p8",
         "Digital Reserves Taken",
         "default-term",
         "income-source",
         "region",
         "channel",
         "price-tier",
         "rate-type",
         "rate",
         "rate-reduction",
         "percent-of-sales",
         "packaging",
         "free-goods",
         "import-status",
         "description",
      ));
      #---------------------
      # ..Now parse the data
      #---------------------
      _processData(\%data);

   } elsif( $self->isTabDelimited( $fileName ) ) {

      report("ArtistContractTemplate::loadMemory -- processing tab-delimited file");

      my %data;
      $reader = Support::Implementation::TabDelimitedReader->new(
         filename => $fileName,
         data => \%data,
         header => \%gTemplateHeader,
         columnmap => \%gColumnMap
      );

      $reader->scanTabbedFile;
      _processData(\%data);

   }
}

#-------------------------------------------------------------------
# _processData is where the real work is done.  It takes the generic
# information stored in the supplied array of hashes and decodes it.
# In this case, it assumes that the supplied data contains contract
# data.
#-------------------------------------------------------------------
sub _processData {

#   die("ArtistContractTemplate -- _processData must be defined in subclass");
   my($data) = @_;
   my $rows = $data->{rows};

   #-----------------------------------------------------------------------
   # Hash of all contracts seen.  We'll use this hash to detect if template
   # lines aren't grouped together properly.
   #-----------------------------------------------------------------------
   my %seenContractMap = ();

   my %excount = (
      payee_not_found     => 0,
      payor_not_found     => 0,
      missing_region      => 0,
      unknown_region      => 0,
      unknown_source      => 0,
      missing_source      => 0,
      invalid_ratetype    => 0,
      unknown_ratetype    => 0,
      missing_ratetype    => 0,
      unknown_channel     => 0,
      missing_channel     => 0,
      unknown_pricelevel  => 0,
      missing_pricelevel  => 0,
      invalid_defaultterm_ratetype => 0,
      unknown_defaultterm_ratetype => 0,
   );

   #----------------------------------------------------------
   # entities: if we create any RPS entities, we'll keep track
   # of the totals in this hash
   #----------------------------------------------------------
   my %entities = (
      new_artist_contract => 0,
      new_artist_contract_term => 0,
      reserve_liquidation => 0,
   );

   #================================================================
   # We'll use the following variables to keep track of the contract
   # whose terms we are processing.
   #================================================================

   #---------------------------------------------------------------------------
   # contractBuffer: this hash will store the term information for the contract
   # currently being read-in from the template.  The hash key is a combination
   # of artistPayeeID and payorID, and references another hash named 'terms',
   # under which is yet another hash indexed by the term priorities.
   #
   # my $key = join("-", $artistPayeeID, $payorID);
   # $contractBuffer{$key}->{rows}->{$rowid} = raw row data
   # $contractBuffer{$key}->{terms}->{0}->{template} = priority 0 row data
   #                               ->{1}->{template} = priority 1 row data
   #
   # The row data is stored under the key "template" as a tab-delimited
   # sequence of term information as read in from the template.
   #
   # During validation, additional sub-hashes may be associated with the
   # priority hashes:
   #       ->{$priority}->{errors} = error message(s) for this term
   #       ->{$priority}->{data} = actual IDs needed to create the term
   #---------------------------------------------------------------------------
   my %contractBuffer=();

   #----------------------------------------------------------------------------
   # currentContractKey: this serves as a key into the 'contractBuffer' hash; is
   # is formed by joining the artistPayeeID and payorID.  This variable is also
   # used to detect when a term for a different contract is being read-in from
   # the template (all term lines for a contract must appear consecutively, with
   # the default term (if defined) appearing after the other terms).
   #----------------------------------------------------------------------------
   my $currentContractKey="";

   #-------------------------------------------------------------------------
   # currentPriority: this is used in building up the hash of terms stored in
   # 'contractBuffer'.  Note that we start numbering at 1.  If a default term
   # definition is encountered in the template, it will be stored in the
   # 'contractBuffer' hash with a priority of 0.
   #-------------------------------------------------------------------------
   my $currentPriority=1;

   # Keep track of the number of contracts processed
   my $contractCounter = 0;

   #--------------------------------------------------------------------
   # errorCode will hold one or more error messages for the current line
   #--------------------------------------------------------------------
   my $errorCode;

   #---------------------------------------------------------------------------
   # Sanity check: Do we have all of the required columns or aliases?
   # If there are any acceptable aliases, you must explicitly check for the alias
   # after first checking for the official column name (see row loop, below).
   #---------------------------------------------------------------------------
   my $nogo = 0;
   if( exists $reader->{missingmap} )
   {
      if( exists $reader->{missingmap}{'term-start'} )
      {
         if( !exists $gTemplateHeader{'term start date'} )  # alias
         {
            print "ERROR: No 'term-start' or 'term start date' found\n";
            $nogo=1;
         }
      }
      if( exists $reader->{missingmap}{'term-end'} )
      {
         if( !exists $gTemplateHeader{'term end date'} )  # alias
         {
            print "ERROR: No 'term-end' or 'term end date' found\n";
            $nogo=1;
         }
      }
   }
   die("ERROR: one or more required columns are missing") if( $nogo );

   #--------------------------------------------------------------
   # Pre-scan the rows and see if there are any lines out of order
   #--------------------------------------------------------------
#   my %termOrder;
#   my %contractError;
#   foreach my $row (@$rows) {
#      my $rowid             = $row->{rowid};
#      my $payeeClientAccount = $row->{'payee-client-account#'}; # 7/16/10
#      my $payeeName         = $row->{'payee-name'};
#      my $rsPayeeID         = $row->{'rs-payee-id'}; # 5/5/10
#      my $contractName      = $row->{'contract-name'};
#      my $artistName        = $row->{'artist-name'};
#      my $payorName         = $row->{'payor-name'};
#
#      #my $contractKey = join("\t", $payeeID, $payorID, $contractName ); # XXX
#      my $rawKey = join("\t", $payeeName, $payeeClientAccount, $payorName, $contractName ); # XXX
#      if(!exists $termOrder{$rawKey})
#      {
#         $termOrder{$contractKey} = $rowid;
#      }
#      else
#      {
#         my $lastRowID = $termOrder{$rawKey};
#         if($rowid != ($lastRowID + 1))
#         {
#             $contractError{$rawKey} = "Template lines out of order";
#         }
#      }
#
#   }

   #---------------------------------------------------------------------------
   # Loop over each row (contract term), and store the term information in the
   # contractBuffer hash.  Once we detect that we're done reading terms for a
   # particular contract, go ahead and try to validate the contract information
   # in the hash.  If it passes muster, create the contract and the terms.
   #---------------------------------------------------------------------------
   foreach my $row (@$rows) {

      #die("STOP\n") if ( $contractCounter > 1 );

      #-------------------------------------------------------
      # Get all of the template variables.
      # Note: the keys listed are defined in %gTemplateHeader.
      #-------------------------------------------------------
      my $rowid             = $row->{rowid};
      my $payeeClientAccount = $row->{'payee-client-account#'}; # 7/16/10
      my $payeeName         = $row->{'payee-name'};
      my $rsPayeeID         = $row->{'rs-payee-id'}; # 5/5/10
      my $contractName      = $row->{'contract-name'};
      my $artistName        = $row->{'artist-name'};
      my $payorName         = $row->{'payor-name'};
      my $issueDate         = $row->{'issue-date'};
      my $clientContractID  = $row->{'contract-id'};
      my $termStart         = $row->{'term-start'} || $row->{'term start date'};
      my $termEnd           = $row->{'term-end'} || $row->{'term end date'};
      my $reservePercentage = $row->{'reserve-percentage'};
      my $p1                = $row->{'p1'};
      my $p2                = $row->{'p2'};
      my $p3                = $row->{'p3'};
      my $p4                = $row->{'p4'};
      my $p5                = $row->{'p5'};
      my $p6                = $row->{'p6'};
      my $p7                = $row->{'p7'};
      my $p8                = $row->{'p8'};
      my $digitalReservesTaken = $row->{'Digital Reserves Taken'}; # 7/16/10
      my $defaultTerm       = $row->{'default-term'};
      my $incomeSource      = $row->{'income-source'};
      my $region            = $row->{'region'};
      my $channel           = $row->{'channel'};
      my $priceTier         = $row->{'price-tier'};
      my $rateType          = $row->{'rate-type'};
      my $rate              = $row->{'rate'};
      my $rateReduction     = $row->{'rate-reduction'};
      my $percentOfSales    = $row->{'percent-of-sales'};
      my $packaging         = $row->{'packaging'};
      my $freeGoods         = $row->{'free-goods'};


if ( $clientID != 182 ) { # 5/7/10 - MOS has _leading_ spaces...
      $payeeName    =~ s/^\s*//g if ( $payeeName );
}
      $payeeName    =~ s/\s*$//g if ( $payeeName );
      $payeeClientAccount    =~ s/\s*$//g if ( $payeeClientAccount );

      $contractName =~ s/\s*$//g if ( $contractName );
      $incomeSource =~ s/^\s*//g if ( $incomeSource );
      $incomeSource =~ s/\s*$//g if ( $incomeSource );
      $artistName   =~ s/^\s*//g if ( $artistName );
      $artistName   =~ s/\s*$//g if ( $artistName );
      $region       =~ s/^\s*//g if ( $region );
      $region       =~ s/\s*$//g if ( $region );
      $channel      =~ s/^\s*//g if ( $channel );
      $channel      =~ s/\s*$//g if ( $channel );
      $priceTier    =~ s/^\s*//g if ( $priceTier );
      $priceTier    =~ s/\s*$//g if ( $priceTier );
      $rateType     =~ s/^\s*//g if ( $rateType );
      $rateType     =~ s/\s*$//g if ( $rateType );
      $rate         =~ s/\s*$//g if ( $rate     );
      $rsPayeeID    =~ s/\s*$//g if ( $rsPayeeID );
      $termStart    =~ s/\s*$//g if ( $termStart);
      $termEnd      =~ s/\s*$//g if ( $termEnd  );
      $payorName    =~ s/(\r|\n)//g if( $payorName );
      $payorName = trimspaces($payorName);


      $payeeName         = '' if (!$payeeName);
      $payeeClientAccount = '' if (!$payeeClientAccount);
      $rsPayeeID         = '' if (!$rsPayeeID);  # 5/5/10
      $contractName      = '' if (!$contractName);
      $artistName        = '' if (!$artistName);
      $payorName         = '' if (!$payorName);
      $issueDate         = '' if (!$issueDate);
      $clientContractID  = '' if (!$clientContractID);
      $termStart         = '' if (!$termStart);
      $termEnd           = '' if (!$termEnd);
      $reservePercentage = '' if (!$reservePercentage);
      $p1                = '' if (!$p1);
      $p2                = '' if (!$p2);
      $p3                = '' if (!$p3);
      $p4                = '' if (!$p4);
      $p5                = '' if (!$p5);
      $p6                = '' if (!$p6);
      $p7                = '' if (!$p7);
      $p8                = '' if (!$p8);
      $digitalReservesTaken   = '' if (!$digitalReservesTaken);
      $defaultTerm       = '' if (!$defaultTerm);
      $incomeSource      = '' if (!$incomeSource);
      $region            = '' if (!$region);
      $channel           = '' if (!$channel);
      $priceTier         = '' if (!$priceTier);
      $rateType          = '' if (!$rateType);
      $rate              = 0  if (!defined $rate or '' eq $rate);
      $rateReduction     = '' if (!$rateReduction);
      $percentOfSales    = '' if (!$percentOfSales);
      $packaging         = '' if (!$packaging);
      $freeGoods         = '' if (!$freeGoods);

      #---------------------------
      # Normalize dates if present
      #---------------------------
      $issueDate = _normalizeDate($issueDate) if ( $issueDate );
      $termStart = _normalizeDate($termStart) if ( $termStart );
      $termEnd   = _normalizeDate($termEnd)   if ( $termEnd );

      #-------------------------------------------------------------------
      # rowData: this is a tab-delimited string containing all of the term
      # information just read-in from the template.  This will be added to
      # the 'contractBuffer' hash.
      #-------------------------------------------------------------------
      my $rowData = join("\t",
         $rowid,
         $payeeClientAccount,  # 7/16/10
         $payeeName,
         $rsPayeeID,  # 5/5/10
         $contractName,
         $artistName,
         $payorName,
         $issueDate,
         $clientContractID,
         $termStart,  # 7/16/10
         $termEnd,  # 7/16/10
         $reservePercentage,
         $p1,
         $p2,
         $p3,
         $p4,
         $p5,
         $p6,
         $p7,
         $p8,
         $digitalReservesTaken,  # 7/16/10
         $defaultTerm,
         $incomeSource,
         $region,
         $channel,
         $priceTier,
         $rateType,
         $rate,
         $rateReduction,
         $percentOfSales,
         $packaging,
         $freeGoods
      );


      report("#### [$rowid]  name($contractName) payee($payeeName) rg($region)"
         . " contractCounter($contractCounter)");


      my $payeeID=0;
      my $payorID=0;
      my $regionID;
      my $contractID;

      #-------------------------------------------------------------------
      # XXX
      # XXX Note: the following artist payee and payor lookups do NOT work
      # XXX as expected if you have duplicate payee or payor names.
      # XXX In the case of a duplicate name, the API will return the first
      # XXX one it fines.  We check for duplicate payee name in _validate.
      # XXX We'e only had issues with duplicate payee names, hence that's
      # XXX all we check for.
      # XXX
      # 9/6/11 - API seems to be working now..
      #-------------------------------------------------------------------

      #----------------------
      # Find the artist payee
      #----------------------

      if ( $rsPayeeID ) {
# 9/13/11 - We use the payeeID 
#         unless( $rsPayeeID =~ /^\d+$/ && $rsPayeeID > 0 ) {
#            die("FATAL ERROR: invalid rsPayeeID($rsPayeeID) detected!!");
#         }
         if ( $rsPayeeID !~ /^\d+$/ || $rsPayeeID <= 0 )
         {
            appendString( $errorCode, "Illegal RS payeeID");
            ++$excount{illegal_rs_payeeid};
print STDERR "D: row($rowid)  Illegal RS payeeID '$rsPayeeID'\n"; # XXX
         }
         else
         {
            $payeeID = $rsPayeeID;
         }

      } elsif ( $payeeName or $payeeClientAccount ) {
         my %args;
         $args{name}              = $payeeName if ( $payeeName );
         $args{client_account_id} = $payeeClientAccount if ( $payeeClientAccount );

         my $payeeObj = RPS::DB::Item::ArtistPayee->Lookup( %args );

         if ( !$payeeObj ) {
            report("ERROR: Payee '". printNull($payeeName) . "' "
               ."clientAccount#(" . printNull($payeeClientAccount) . "' "
               ." not found");
            #appendString( $errorCode, "Payee not found");  # moved to _validate
            #++$excount{payee_not_found};
         } else {
            $payeeID = $payeeObj->artist_payee_id;
         }
      } else {
         report("ERROR: No payee info");
      }

#die("payeeID not defined for payeeName($payeeName) rsPayeeID($rsPayeeID)!!!\n") if ( !$payeeID );

      #---------------
      # Find the payor
      #---------------
      my $payorObj = RPS::DB::Item::Payor->Lookup(
         name => $payorName,
      );
      if ( !$payorObj ) {
         #appendString( $errorCode, "Payor not found"); # moved to _validate
         #++$excount{payor_not_found};
      } else {
         $payorID = $payorObj->payor_id;
      }


      my $contractKey = join("\t", $payeeID, $payorID, $contractName ); # XXX

      if ( '' eq $currentContractKey ) {
         # This is the first term of the first contract in the template
         $currentContractKey = $contractKey;
      }


      if ( $currentContractKey eq $contractKey ) {

         #-----------------------------------------------------------------
         # Still processing the same contract, so just store the term info.
         #-----------------------------------------------------------------

         my $err = _storeTerm(
            $defaultTerm,
            \$currentPriority,
            $rowData,
            #\%{$contractBuffer{$currentContractKey}->{terms}}
            \%{$contractBuffer{$currentContractKey}}
         );

         if ( $err ) {
            report("DEBUG: rowid($rowid) has err($err)");
            if ( $err == kDefaultTermNotLast ) {
               appendString( $errorCode, "Default term order error");
               ++$excount{default_term_order_error};
            } elsif( $err == kDuplicateTerm ) {
               appendString( $errorCode, "Duplicate default terms");
               ++$excount{duplicate_default_terms};
               #die("default term not last!!!\n");
            } else {
               die("unknown error '$err'\n");
            }
         }

      } else {
         #----------------------------------------------------------
         # Ok, the current template line is referring to a different
         # contract.  We must validate whatever's in the contract
         # buffer before storing the current template line.
         #----------------------------------------------------------
         report(">>> Different contract key detected on rowid($rowid)..."
            . " validating current contract buffer");

         #--------------------------------------------------------------------
         #
         # Check if we've already seen the contract we're about to validate...
         #
         #--------------------------------------------------------------------

         #------------------------------------------------------------------
         # newErrorCode - this will apply to the _next_ contract (the one
         # referenced by 'contractKey', not the current contract (referenced
         # by 'currentContractKey').
         #------------------------------------------------------------------
         my $newErrorCode;

         if ( exists $seenContractMap{$contractKey} ) {
            appendString( $newErrorCode, "Template line out of order");
            ++$excount{template_line_out_of_order};
         }

         #--------------------------
         #
         # Now validate the contract
         #
         #--------------------------
         _validate( $errorCode, \%entities, \%excount, \%contractBuffer );

         report("##################################################");
         report("#### starting new contract: rowData<$rowData>\n");
         report("#### contractKey($contractKey)");

         #----------------------------------------------------------
         # Since the contract has been validated, mark it as 'seen'.
         # Note that we only do this _once_ per group of contracts.
         #----------------------------------------------------------

         report("DEBUG: setting seenContractMap{$currentContractKey} = 1 ");
         $seenContractMap{$currentContractKey} = 1;

         #--------------------------
         # reset the contract buffer
         #--------------------------
         undef %contractBuffer;
         $currentPriority = 1;
         $currentContractKey = $contractKey;

         # Propagate the error (if any) for the new current contract
         undef $errorCode;
         $errorCode = $newErrorCode if ( $newErrorCode );

         #--------------
         # Next contract
         #--------------
         ++$contractCounter;

         #-----------------------
         # store the current term
         #-----------------------
         my $err = _storeTerm(
            $defaultTerm,
            \$currentPriority,
            $rowData,
            \%{$contractBuffer{$currentContractKey}}
         );
      }
      
   }# template row loop

   report("DONE reading template rows...");

   #-----------------------------
   # Process the last contract...
   #-----------------------------
   if ( %contractBuffer ) {
      report("Examining contract buffer...");

      foreach my $cbkey (keys %contractBuffer) {
         report("DEBUG: cbkey($cbkey)");

         my %terms = %{$contractBuffer{$cbkey}->{terms}};
         report("   There are ".(keys %terms)." term(s):");
         foreach my $term (keys %{$contractBuffer{$cbkey}->{terms}} ) {
            report("   DEBUG: term($term)");
         }
      }

      report("...calling _validate");

      _validate( $errorCode, \%entities, \%excount, \%contractBuffer );
   }

   #-----------------------
   # Show import statistics
   #-----------------------
   report("##### S U M M A R Y #####");
   report("Entities Created:");
   foreach my $c (keys %entities) {
      my $v = $entities{$c};
      printf("%30s %6d\n", $c, $v);
   }
   report("Exceptions:");
   my $totalExceptions = 0;
   my $totalRows = (scalar @$rows);
   foreach my $c (keys %excount) {
      my $v = $excount{$c};
      printf("%30s %6d\n", $c, $v);
      $totalExceptions += $v;
   }
   printf("%30s %s\n", " ", "-------" );
   printf("%30s %6d\n", "Total Exceptions", $totalExceptions );
   printf("%30s %6d\n", "Total Rows", $totalRows );
   report(" ");

}#_processData

# Name: _storeTerm
# Description: This subroutine stores row information in the
# supplied contract buffer.
# Arguments:
#   defaultTerm: flag, if 'x' then term will be stored as default
sub _storeTerm {
   my ( $defaultTerm, $currentPriority, $rowData, $rowBuffer ) = @_;
   my $retval;

   my($rowid) = split("\t",$rowData);

   if ( $defaultTerm && (lc $defaultTerm) eq 'x') {
      #-------------
      # default term
      #-------------

      #$rowBuffer->{0}->{template} = $rowData;
report("_storeTerm: processing default term");

      if ( exists ${_[3]}{terms}->{0}->{processed}  ) { # default term exists
         # TODO: store error
         $retval = kDuplicateTerm;
         report("_storeTerm: default term already exists?\n");

         #----------------------------------------------------
         # Store the row data and add an error message
         #
         # Note that we don't store any term information since
         # we already have a default term.
         #----------------------------------------------------
         ${_[3]}{rows}->{$rowid}->{line} = $rowData;
         ${_[3]}{rows}->{$rowid}->{errors} = "Duplicate term";

      } else {

         #--------------------------------------------------
         # Store the row data and mark the term as processed
         #--------------------------------------------------
         ${_[3]}{terms}->{0}->{processed} = 1;
         ${_[3]}{rows}->{$rowid}->{line} = $rowData;
         ${_[3]}{terms}->{0}->{rowid} = $rowid;
         report("_storeTerm:  ..Storing default term: rowData($rowData)");


         #report("DEBUG: _storeTerm: default term sanity check: rowData<". ${_[3]}->{0}->{template} .">");
         report("DEBUG: _storeTerm: default term sanity check: rowData<". ${_[3]}{rows}->{$rowid}->{line} .">");
      }
   } else {
      #-----------------
      # non-default term
      #-----------------
report("_storeTerm: processing term, cp($$currentPriority)");
      #------------------------------------------------------------------
      # Not a default term.  Note that we check if a default term already
      # exists, because if it does then the terms are out sequence (the
      # default must the last term defined for any contract.
      #------------------------------------------------------------------
      #if ( exists $rowBuffer->{0}->{template} ) 

      #if ( exists $rowBuffer->{terms}->{0}->{processed} &&   -- causes '0' key to be created???
      #   $rowBuffer->{terms}->{0}->{processed} == 1 )

      if ( exists $rowBuffer->{terms}->{0} &&
         exists $rowBuffer->{terms}->{0}->{processed} &&
         $rowBuffer->{terms}->{0}->{processed} == 1 ) {
         # TODO: store error
         #die("TODO: ERROR - default term isn't last term");
         report("TODO: ERROR - default term isn't last term");
         $retval = kDefaultTermNotLast;
         ${_[3]}{rows}->{$rowid}->{line} = $rowData;
         ${_[3]}{rows}->{$rowid}->{errors} = "Default term not last";

         #---------------------------------------------------------------------
         # Note: if we're in this section of code, then the current term won't
         # be stored in the 'terms' hash since it isn't valid.  We need to mark
         # the default term with the error.
         #---------------------------------------------------------------------

         # Get the rowid of the default term:
         my $drowid = $rowBuffer->{terms}->{0}->{rowid};
         ${_[3]}{rows}->{$drowid}->{errors} = "Default term not last";
      } else {

         #$rowBuffer->{$$currentPriority}->{template} = $rowData;

         my $pri = $$currentPriority;

         die("zero priority detected") if ( !$pri || $pri == 0 );
         #${_[3]}{$pri}->{template} = $rowData;
         ${_[3]}{rows}->{$rowid}->{line} = $rowData;
         ${_[3]}{terms}->{$pri}->{processed} = 1;
         ${_[3]}{terms}->{$pri}->{rowid} = $rowid;

         report("_storeTerm:  ..Storing term pri($pri) cp[$$currentPriority], rowData($rowData)");

         $$currentPriority++;

         #report("DEBUG: _storeTerm: sanity check: rowData<". ${_[3]}{rows}->{$rowid}->{line} .">");

         #if ( $pri == 1 ) { # DEBUG - dump out 'terms' hash
         #   my %terms = %{${_[3]}{terms}};
         #   report("DEBUG: _storeTerm: terms hash: " . Dumper(\%terms) );
         #}
      }
   }
   return $retval;
}#_storeTerm

#------------------------------------------------------------------------
# Name: _validate
# Description:
#   This method is responsible for validating a contract and its terms.
#   If everything checks out, then the contract is created (if it doesn't
#   exist) and then the term(s) are also created.  If there are any
#   errors, then no contract or term(s) are created, and an exception
#   line (starts with "STATUS") is generated.
# Arguments:
#   emsg: an optional global error for the contract
#   entities: hash reference to entities counter hash
#   excount: hash reference to exception counter hash
#   buffer: the contract buffer to validate
#------------------------------------------------------------------------
sub _validate {
   my($emsg, $entities, $excount, $buffer) = @_;

   my $grossRevenueEnabled = RPS::DB::Item::ClientOptions->Get( 'gross_revenue' );

   report("### _validate, emsg(" . printNull($emsg) . ")");
   #--------------------------------------------------------------------
   # We must make two passes; the first pass ensures that all of the
   # contract's entities are valid; the second pass creates the entities
   # if everything was successfully validated in the first pass.
   #--------------------------------------------------------------------

   #------------------------------------------------------------------
   # contractHasErrors: set to 1 if contract or any term has an error
   # This will prevent the contract (or its terms) from being created
   # in the second pass.
   #------------------------------------------------------------------
   my $contractHasErrors;

   my $payeeValidated; # set once the payee has been validated
   my $payorValidated; # set once the payor has been validated

   #-----------------
   # .. First pass ..
   #-----------------

   #----------------------------------------------------------------
   # contractNameMap: each term should have the same contract name
   # on it.  This hash is used to detect if there are term(s) with
   # inconsistent contract names.  Similarly, we use 'artistNameMap'
   # to check for artist name uniqueness.
   #----------------------------------------------------------------
   my %contractNameMap;
   my %artistNameMap;
   my %issueDateMap;
   my %termStartMap;
   my %termEndMap;
   my %clientContractMap;
   my %reservePercentageMap;
   my %scheduleMap;
   my %rsPayeeIDMap;


   # 7/7/09: Rate reductions weren't being set for non-physical terms.
   # This is incorrect; we need to check all terms (except the default)
   # and see if the rate reduction is really the same for all non-default
   # terms.
   my %rateReductionMap; # TEMPORARY

   # Note: the way the contractBuffer is managed, we'll only
   # have one contractKey to deal with.  Do we really need a
   # foreach-loop here? -ES

   foreach my $contractKey (keys %$buffer) {

      #my($payeeID,$payorID,$contractName) = split("-", $contractKey);
      my($payeeID,$payorID,$contractName) = split("\t", $contractKey); # XXX

      #------------------------------------------------------------
      # If we have a contract-level error, it will appear on all of
      # template lines associated with its terms
      #------------------------------------------------------------
      my $globalErrorCode = $emsg;

      #-------------------------------
      # Validate the payee information
      #-------------------------------
      if ( !$payeeID || $payeeID == 0) {
         appendString( $globalErrorCode, "Payee not found");
         ++$excount->{payee_not_found};
      }

# Moved below into term loop
#      my ($_payeeName) = $gPayeeIDMap{$payeeID};
#      if ( $gPayeeCountMap{$_payeeName} > 1 ) {
#         appendString( $globalErrorCode, "Duplicate payee name");
#         ++$excount->{duplicate_payee_name};
#      }


      if ( !$payorID || $payorID == 0) {
         appendString( $globalErrorCode, "Payor not found");
         ++$excount->{payor_not_found};
      }
      
      #die("DEBUG: _validate:  NS196 term buffer : " . Dumper(\%$buffer))
      #   if ( $contractName eq 'NS196' );

      report("   _validate: contractKey($contractKey)...");
      #my $terms = $buffer->{$contractKey}->{terms};
      my %terms = %{$buffer->{$contractKey}->{terms}};
      my $ic=0;
      report("   There are ".(keys %terms)." term(s):");

      #------------------------------------------------------------------
      # errorCode: this will contain the error message(s) associated with
      # with the term currently being validated.  It will be added to the
      # term's hash using the 'errors' key.
      #------------------------------------------------------------------
      #my $errorCode;

      #---------------------------------------------------------------------
      # seenPriorityMap: This simply keeps track of the term priorities that
      # have been processed.  We use this to check for duplicate default
      # terms (the non-default terms won't have duplicated priority values).
      #---------------------------------------------------------------------
      my %seenPriorityMap;

      #------------------------------------------------------------------------
      # seenTermMap: This keeps track of the terms that we've seen.  It is used
      # to check if the user has duplicated any terms.  A term is duplicate if
      # it has the same source, region, channel, price and rate type as another
      # term.
      #------------------------------------------------------------------------
      my %seenTermMap;

      #---------------------
      # Term validation loop
      #---------------------
      foreach my $priority (sort { $a <=> $b } keys %terms) {

         my $zzRowid = $terms{$priority}->{rowid};

         my $rowData = $buffer->{$contractKey}->{rows}->{$zzRowid}->{line};

         my($rowid,
            $payeeClientAccount, # 7/16/10
            $payeeName,
            $rsPayeeID, # 5/5/10
            $contractName,
            $artistName,
            $payorName,
            $issueDate,
            $clientContractID,
            $termStart, # 7/16/10
            $termEnd, # 7/16/10
            $reservePercentage,
            $p1,
            $p2,
            $p3,
            $p4,
            $p5,
            $p6,
            $p7,
            $p8,
            $digitalReservesTaken, # 7/16/10
            $defaultTerm,
            $incomeSource,
            $region,
            $channel,
            $priceTier,
            $rateType,
            $rate,
            $rateReduction,
            $percentOfSales,
            $packaging,
            $freeGoods) = split("\t",$rowData);

#report("DEBUG_1ST: priority($priority) zzRowid($zzRowid) rowid($rowid) contractName($contractName)");
         #------------------------------------------------------------------
         # errorCode: this will contain the error message(s) associated with
         # with the term currently being validated.  It will be added to the
         # term's hash using the 'errors' key.
         #------------------------------------------------------------------
         my $errorCode;

         #----------------------------
         # Get any pre-existing errors
         #----------------------------
         #$errorCode = $terms{$priority}->{errors} if ( exists $terms{$priority}->{errors} );
         #if ( exists $terms{$priority}->{errors} ) 
         if ( exists $buffer->{$contractKey}->{rows}->{$zzRowid}->{errors} ) {
            $errorCode = $buffer->{$contractKey}->{rows}->{$zzRowid}->{errors};
            #$errorCode = $terms{$priority}->{errors};
            $contractHasErrors = 1;
         }
         if ( $globalErrorCode ) {
            appendString( $errorCode, $globalErrorCode);
            $contractHasErrors = 1;
         }


         #----------------------------------
         # Check if the payee name is unique
         #----------------------------------
         if ( !$rsPayeeID ) {
            my $_payeeName = (defined $payeeID && $payeeID != 0 ) ? lc $gPayeeIDMap{$payeeID} : undef;

            report("WARNING: No payeeID for payeeName '$payeeName'") if(!$payeeID); # XXX
            report("_payeeName(". printNull($_payeeName) . ") payeeID(" . printNull($payeeID) . ")");

            #my $k = join( "\t", $_payeeName, $payeeClientAccount ) || undef;
            my $k;
            if ( $_payeeName ) {
               if ( $payeeClientAccount ) {
                  $k = join( "\t", $_payeeName, $payeeClientAccount );
               } else {
                  $k = join( "\t", $_payeeName, '' );
               }
            }

            # Check for duplicate payee name.  If the payee client account is present then
            # we check for duplicate payee name and client account.
            #
            if ( !$payeeClientAccount )
            {
               if ( $_payeeName && exists $gPayeeCountMap{$_payeeName} && $gPayeeCountMap{$_payeeName} > 1 ) {
                  appendString( $errorCode, "Duplicate payee name");
                  ++$excount->{duplicate_payee_name};
                  report("_validate: payee '$_payeeName' is a duplicate!");
               }
               $payeeClientAccount = '' if ( !$payeeClientAccount );
            }
            else
            {
               # This shouldn't happen, but we don't enforce unique name/client
               # account ID combinations
               # 
               if ( $_payeeName && $k && exists $gPayeeAccountCountMap{$k} && $gPayeeAccountCountMap{$k} > 1 ) {
                  appendString( $errorCode, "Duplicate payee name");
                  ++$excount->{duplicate_payee_name};
                  report("_validate: payee '$_payeeName' (clientAccountID '$payeeClientAccount') is a duplicate!");
               }
            }

         } else {
            my ($actualPayeeName) = $gPayeeIDMap{$rsPayeeID};

            my $cleanActualName   = clean($actualPayeeName);
            my $cleanTemplateName = clean($payeeName);

            if ( !$actualPayeeName )
            {
               appendString( $errorCode, "Invalid RS payeeID");
               ++$excount->{invalid_rs_payeeid};
               report("DEBUG: invalid RS payeeID $rsPayeeID");
            }
#            elsif( lc $actualPayeeName ne lc $payeeName )
            elsif( $cleanActualName ne $cleanTemplateName )
            {
               appendString( $errorCode, "Payee name mismatch");
               ++$excount->{payee_name_mismatch};
               report("DEBUG: payee_name_mismatch(id=$rsPayeeID): template($payeeName) != actual($actualPayeeName)");
            }
         }


#         report("   DEBUG: PRI($priority) term($ic) row($contractKey) rowid($rowid)... KEY($rowData)");

         my $regionID;
         my $formatID;
         my $incomeSourceID;
         my $channelID;
         my $contractRateTypeID;
         my $priceLevelID;

         my $contractTermSourceID;       # NEW 7/7/10
         my $parentContractTermSourceID; # NEW 7/7/10 (unused for now)

         my $seenContractRateTypeID;
         my $seenPriceLevelID;

         # We need some unvalidated values to help in checking for duplicated
         # terms in the template.  These are used only for dupe checking.
         #
         if ( $priceTier && exists $gPriceLevelMap{lc $priceTier} ) {
            $seenPriceLevelID = $gPriceLevelMap{ lc $priceTier };
         }
         if ( $rateType )
         {
            if ( exists $gContractRateTypeMap{lc $rateType} ) {
               $seenContractRateTypeID = $gContractRateTypeMap{lc $rateType};
            } else {
               #die("INVALID RATE TYPE DETECTED: '$rateType' : ". Dumper(\%gContractRateTypeMap) );
               appendString( $errorCode, "Invalid rate type");
               ++$excount->{invalid_rate_type};
               report("INVALID_RATETYPE($rateType)");
            }
         }


         if ( "0" ne $priority ) {
            #-----------------
            # Non-default term
            #-----------------

            #--------------------
            # Validate the region
            #--------------------
            if ( '' ne $region ) {
               if ( exists $gRegionMap{lc $region} ) {
                  $regionID = $gRegionMap{ lc $region };
               } else {
                  appendString( $errorCode, "Unknown region");
                  ++$excount->{unknown_region};
                  report("UNKNOWN_REGION($region)");
               }
            } else {
               appendString( $errorCode, "Missing region");
               ++$excount->{missing_region};
            }

            #---------------------------
            # Validate the income source
            #---------------------------
            if ( '' ne $incomeSource ) {
               if ( exists $gIncomeSourceMap{ lc $incomeSource } ) {
                  ($formatID, $incomeSourceID) = split("\t", $gIncomeSourceMap{ lc $incomeSource } );

                  #-----------------------------
                  # Set the contract term source
                  #-----------------------------
                  ($contractTermSourceID, $parentContractTermSourceID) = split("\t",
                     $gContractTermSourceMap{ lc $incomeSource }
                  );
		  print STDERR "Invalid incomeSource($incomeSource) when looking at ".
		     Dumper(\%gContractTermSourceMap) . "\n" if( !$contractTermSourceID );

                  assert($contractTermSourceID);

               } else {
                  report("ERROR: Unknown source '$incomeSource'");
                  appendString( $errorCode, "Unknown source");
                  ++$excount->{unknown_source};
               }


            } else {
               report("ERROR: Missing income source");
               appendString( $errorCode, "Missing source");
               ++$excount->{missing_source};
            }

#assert($incomeSourceID);

            #--------------------------------
            # Validate the contract rate type
            #--------------------------------
            if ( '' ne $rateType ) {
               $rateType =~ s/ //g;
               if ( exists $gContractRateTypeMap{lc $rateType} ) {
                  my $zzRateID = $gContractRateTypeMap{lc $rateType};

                  #-----------------------------------------------
                  # The rate type must be valid for the specified
                  # income source.
                  #-----------------------------------------------
                  report("DEBUG: incomeSourceID($incomeSourceID) zzRateID($zzRateID)");

                  if ( $incomeSourceID ) {
                     if ( _isValidContractRateType($incomeSourceID, $zzRateID) ) {
                        $contractRateTypeID = $zzRateID;
                     } else {
                        appendString( $errorCode, "Invalid rate type for source");
                        ++$excount->{invalid_ratetype_for_source};
                     }
                  }
                  else
                  {
                     # "All" income source is compatible with all rate types
                     $contractRateTypeID = $zzRateID;
                  }

                  #-----------------------------------------------
                  # The % gross revenue rate type is only valid
                  # for clients who have that option enabled.
                  #-----------------------------------------------
                  if ($zzRateID == RPS::DB::Item::ContractRateType::kRateTypePercentGrossRevenue && !$grossRevenueEnabled ) {
                     appendString( $errorCode, "Invalid rate type");
                     ++$excount->{invalid_ratetype};
                  }

               } else {
                  report("ERROR: Unknown rate type '$rateType'");
                  appendString( $errorCode, "Unknown rate type");
                  ++$excount->{unknown_ratetype};
               }
            } else {
               appendString( $errorCode, "Missing rate type");
               ++$excount->{missing_ratetype};
            }

            #-----------------------------------------------------
            # Validate channel and price only for physical formats
            #-----------------------------------------------------
            if ( $formatID and ($formatID == 1 or $formatID == 0 ) ) {
               die("incomeSourceID not defined?!") if (!$incomeSourceID);
               #-----------------
               # Validate channel
               #-----------------
               if ( '' ne $channel && _isPriceLevelAllowed($incomeSourceID) ) {

                  if ( exists $gChannelMap{lc $channel} ) {
                     $channelID = $gChannelMap{ lc $channel };
                  } else {
                     appendString( $errorCode, "Unknown channel");
                     ++$excount->{unknown_channel};
                  }
               } else {
                  appendString( $errorCode, "Missing channel");
                  ++$excount->{missing_channel};
               }

               #---------------------
               # Validate price-level
               #---------------------
               if ( '' ne $priceTier && _isPriceLevelAllowed($incomeSourceID) ) {
               
                  if ( exists $gPriceLevelMap{lc $priceTier} ) {
                     $priceLevelID = $gPriceLevelMap{ lc $priceTier };
                  } else {
                     appendString( $errorCode, "Unknown price level");
                     ++$excount->{unknown_pricelevel};
                  }
               } else {
	          report("ERROR: missing price level; priceTier($priceTier) incomeSourceID($incomeSourceID)");
                  appendString( $errorCode, "Missing price level");
                  ++$excount->{missing_pricelevel};
               }

               #my $pattern =
               #/^[+]? (
               #  \d+\.\d+ |  # NNN.NNN
               #  \d+\.    |  # NNN.
               #  \.\d+       # .N
               #  )
               #/x;

               ## TODO: Validate the rate reduction
               #$terms{$priority}->{data}->{rate_reduction} = $rateReduction;

               ## TODO: Validate the percentOfSales
               #$terms{$priority}->{data}->{percent_of_sales} = $percentOfSales;

               # TODO: Validate the packaging
               $terms{$priority}->{data}->{packaging_deduction} = $packaging;

            } elsif( $formatID and $formatID == 2 ) {
               #---------------------------------------------------------------
               # Digital income source - if the template contains a channel
               # or price level then flag it with an exception.
               # You cannot define the channel or price level on digital terms.
               #---------------------------------------------------------------
               if ( '' ne $priceTier and 'all' ne (lc $priceTier) ) { # 5/5/10
                  appendString( $errorCode, "Price level not allowed on digital source");
                  ++$excount->{price_level_not_allowed_on_digital_source};
               }

               if ( '' ne $channel and 'all' ne (lc $channel) ) { # 5/5/10
                  appendString( $errorCode, "Channel not allowed on digital source");
                  ++$excount->{channel_not_allowed_on_digital_source};
               }
            } elsif( $formatID and $formatID == 3 ) {
               #---------------------------------------------------------------
               # Performance income source - if the template contains a channel
               # or price level then flag it with an exception.
               # You cannot define the channel or price level on PI terms.
               #---------------------------------------------------------------
               if ( '' ne $priceTier ) {
                  appendString( $errorCode, "Price level not allowed on PI source");
                  ++$excount->{price_level_not_allowed_on_PI_source};
               }

               if ( '' ne $channel ) {
                  appendString( $errorCode, "Channel not allowed on PI source");
                  ++$excount->{channel_not_allowed_on_PI_source};
               }
            } else {
               # invalid source .. no format...
            }


            # TODO: Validate the percentOfSales
            $terms{$priority}->{data}->{percent_of_sales} = $percentOfSales;

            # Allow free goods on all format types (RSD-9305)
            # TODO: Validate the freeGoods
            $terms{$priority}->{data}->{free_goods_deduction} = $freeGoods;

         } else {
            #----------------------------------------------------
            # For a default term, validate the contract rate type
            #----------------------------------------------------
            if ('' ne $rateType) {
               if ( exists $gContractRateTypeMap{lc $rateType} ) {
                  my $zzRateID = $gContractRateTypeMap{lc $rateType};
                  if ( $zzRateID == RPS::DB::Item::ContractRateType::kRateTypePercentRevenue ) {
                     $contractRateTypeID = $zzRateID;
                  } else {
                     appendString( $errorCode, "Invalid rate type for default term");
                     ++$excount->{invalid_defaultterm_ratetype};
                  }
               } else {
                  report("ERROR: Unknown default rate type '$rateType'");
                  appendString( $errorCode, "Unknown default term_rate type");
                  ++$excount->{unknown_defaultterm_ratetype};
               }
            } else {
               #---------------------------------------------------
               # If not specified, set the rate type to net revenue
               #---------------------------------------------------
               $contractRateTypeID =
                  RPS::DB::Item::ContractRateType::kRateTypePercentRevenue;
            }

         }

         #-----------------------------------------------------------
         #
         # For terms with a non-payable rate type, make sure that we
         # have the fields that we need. 7/16/10
         #
         # Note: the validation for payable terms is being left the
         # same as it was before the non-payable feature was enabled.
         # That is, refer to the validation logic above.
         #
         #-----------------------------------------------------------
         if ( $contractRateTypeID and $contractRateTypeID == RPS::DB::Item::ContractRateType::kRateTypeNonPayable ) {

            if ( $contractTermSourceID == 2 ) { # 'all physical' (2)

               # If physical, then we must have a region, channel, price tier and rate type

               if ( !$regionID and !$channelID and
                    !$priceLevelID and !$contractRateTypeID ) {

                  report("ERROR: Non-payable 'all physical' term missing region/channel/price/rateType");
                  appendString( $errorCode, "Non-payable all physical term missing required fields");
                  ++$excount->{nonpayable_physical_missing_fields};
               }

            } elsif( $contractTermSourceID == 1 or
                     $contractTermSourceID == 3 ) { # 'all' (1) or 'all digital' (3)

               # If digital, then we must have a region and rate type

               if ( !$regionID and !$contractRateTypeID ) {

                  report("ERROR: Non-payable 'all/all digital' term missing region/rateType");
                  appendString( $errorCode, "Non-payable all/all digital term missing required fields");
                  ++$excount->{nonpayable_digital_missing_fields};
               }
            }
         }


         #==========================
         #
         # Contract-level validation
         #
         #==========================

         #------------------
         # Validate the rate
         #------------------
         die("STOP: rate undefined rowid($rowid)") if ( ! defined $rate );

         if ( $rate =~ m/^-/ ) {
            report("ERROR: Negative rate '$rate'");
            appendString( $errorCode, "Negative rate");
            ++$excount->{negative_rate};
         } else {
            #$rate =~ s/\./0./;  # prepend zero if user entered fractional number

            # /^-?(?:\d+(?:\.\d*)?|\.\d+)$/
            # /^
            #   -  # negative, followed by
            #   ?
            #   (?: \d+      # one or more numbers..
            #     (?:\.\d*)  # ..followed by decimal and zero or more numbers, _OR_
            #     ?|\.\d+)   # ..followed by decimal and one or more numbers
            # $/\x
            if ( $rate !~ /^-?(?:\d+(?:\.\d*)?|\.\d+)$/ ) {

               report("  row($rowid): invalid rate: '$rate'\n");
               appendString( $errorCode, "Invalid rate");
               ++$excount->{invalid_rate};

            } elsif( 0 == $rate ) {
# Allow zero rate terms (FB14987)
#               if ( $contractRateTypeID and $contractRateTypeID != RPS::DB::Item::ContractRateType::kRateTypeNonPayable ) {
#                  report("  row($rowid): zero rate: '$rate'\n");
#                  appendString( $errorCode, "Zero rate");
#                  ++$excount->{zero_rate};
#               }
               $terms{$priority}->{data}->{rate} = $rate;
            } else {
               if ( $contractRateTypeID and
                    $contractRateTypeID == RPS::DB::Item::ContractRateType::kRateTypeNonPayable and
                    $rate != 0 ) {
                  appendString( $errorCode, "Non-payable term with rate");
                  ++$excount->{nonpayable_term_with_rate};
               } else {
                  $terms{$priority}->{data}->{rate} = $rate;
               }
            }

         }

         #----------------------------------
         # TODO: Validate the rate reduction
         #----------------------------------
         $terms{$priority}->{data}->{rate_reduction} = $rateReduction;

         if ( $priority > 0 ) {
            $rateReductionMap{$rateReduction} = 1; # TEMPORARY -- need to check for multiple rate reductions 7/7/09
         }

         #----------------------------------
         # Make sure we have a contract name
         #----------------------------------
         #die("STOP: contractName($contractName)\n");
         if ( !$contractName ) {
            appendString( $errorCode, "Missing contract name");
            ++$excount->{missing_contract_name};
            $contractNameMap{""} = "";
         } else {
            $terms{$priority}->{data}->{contract_name} = $contractName;
            # Store name; we'll check for name consistency in the 2nd validation pass
            $contractNameMap{$contractName} = $contractName;
         }

         #-----------------------------------------------------------------------
         # Artist name is optional, but if defined we'll check it for consistency
         #-----------------------------------------------------------------------
         if ( !$artistName ) {  # artist name is optional
            #appendString( $errorCode, "Missing contract name");
            #++$excount->{missing_contract_name};
            $artistNameMap{""} = "";
         } else {
            $terms{$priority}->{data}->{artist_name} = $artistName;
            # Store name; we'll check for name consistency in the 2nd validation pass
            $artistNameMap{$artistName} = $artistName;
         }

         #---------------------------------------------------------------------
         # rsPayeeID is optional, but if defined we'll check it for consistency
         # Note: the rsPayeeID is stored in the data hash.  When we create the
         # contract, a non-null rsPayeeID will override the payeeID stored in
         # the contract key. -5/5/10
         #---------------------------------------------------------------------
         if ( !$rsPayeeID ) {  # artist name is optional
            #appendString( $errorCode, "Missing contract name");
            #++$excount->{missing_contract_name};
            $rsPayeeIDMap{""} = "";
         } else {
            $terms{$priority}->{data}->{rs_payee_id} = $rsPayeeID;
            $rsPayeeIDMap{$rsPayeeID} = $rsPayeeID;
         }


         #-------------------------------
         # Validate issue date if present
         #-------------------------------
         if ( '' ne $issueDate ) {
            $issueDate =~ s/ //g;
            if ( ! _isValidDate($issueDate) ) {
               appendString( $errorCode, "Invalid issue date");
               ++$excount->{invalid_issue_date};
            } else {
               $terms{$priority}->{data}->{issue_date} = $issueDate;
            }
            $issueDateMap{$issueDate} = $issueDate;
         } else {
            $issueDateMap{""} = "";
         }

         #-------------------------------
         # Validate term start if present - 7/16/10
         #-------------------------------
         if ( '' ne $termStart ) {
            $termStart =~ s/ //g;
            if ( ! _isValidDate($termStart) ) {
               report("DEBUG: Term Validation: invalid term-start '$termStart'");
               appendString( $errorCode, "Invalid term start");
               ++$excount->{invalid_term_start};
            } else {
               $terms{$priority}->{data}->{term_start} = $termStart;
            }
            $termStartMap{$termStart} = $termStart;
         } else {
            $termStartMap{""} = "";
         }

         #-----------------------------
         # Validate term end if present - 7/16/10
         #-----------------------------
         if ( '' ne $termEnd ) {
            $termEnd =~ s/ //g;
            if ( ! _isValidDate($termEnd) ) {
               report("DEBUG: Term Validation: invalid term-end '$termEnd'");
               appendString( $errorCode, "Invalid term end");
               ++$excount->{invalid_term_end};
            } else {
               $terms{$priority}->{data}->{term_end} = $termEnd;
            }
            $termEndMap{$termEnd} = $termEnd;
         } else {
            $termEndMap{""} = "";
         }

         #-------------------------------------------------------
         # If client contractID is defined then it must be unique
         #-------------------------------------------------------
         if ( '' ne $clientContractID ) {
            $terms{$priority}->{data}->{client_contract_id} = $clientContractID;
            $clientContractMap{$clientContractID} = $clientContractID;
         } else {
            $clientContractMap{""} = "";
         }

         if ( $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceOrchardSync ) {
             if ( $reservePercentage ) {
                 appendString( $errorCode, "Reserves can't be configured for this source");
                 ++$excount->{sync_reserves};
             }
             if ( $p1 || $p2 || $p3 || $p4 || $p5 || $p6 || $p7 || $p8 ) {
                 appendString( $errorCode, "Reserves can't be defined for this source");
                 ++$excount->{sync_reserves_schedule};
             }
         }

# Commented-out isPhysical -- this will ensure that the liquidation
# schedule will be properly applied to the contract.  Note that we
# do not check if the contract is specifying a reserve rate and
# schedule without any physical terms. -ES 5/11/10
#         if ( _isPhysical($incomeSourceID) ) {

            #-------------------------------------------------------
            # If reserve percentage is defined then it must be unique
            #-------------------------------------------------------
            if ( '' ne $reservePercentage ) {
               $reservePercentageMap{$reservePercentage} = $reservePercentage;
            } else {
               #die("row($rowid):STOP: reservePercentage($reservePercentage)");
               $reservePercentageMap{""} = "";
            }

            #----------------------------------
            # Validate the liquidation schedule
            #----------------------------------
            my $schedKey = join("-",$p1, $p2, $p3, $p4, $p5, $p6, $p7, $p8);
#report("DEBUG: srcID($incomeSourceID) _schedKey($schedKey)");
            $scheduleMap{$schedKey} = 1; # Used to check for uniqueness
            my $smsg = _isValidSchedule($schedKey);
            if ( $smsg ) {
               appendString( $errorCode, $smsg);
               ++$excount->{invalid_schedule};
report("invalid_schedule: smsg($smsg) errorCode($errorCode)");
            } else {
               $terms{$priority}->{data}->{p1} = $p1 if ( $p1 );
               $terms{$priority}->{data}->{p2} = $p2 if ( $p2 );
               $terms{$priority}->{data}->{p3} = $p3 if ( $p3 );
               $terms{$priority}->{data}->{p4} = $p4 if ( $p4 );
               $terms{$priority}->{data}->{p5} = $p5 if ( $p5 );
               $terms{$priority}->{data}->{p6} = $p6 if ( $p6 );
               $terms{$priority}->{data}->{p7} = $p7 if ( $p7 );
               $terms{$priority}->{data}->{p4} = $p8 if ( $p8 );
            }
#         }

         #---------------------------------------------------------------------------
         # If Digital Reserves is specified, then we need both a liquidation schedule
         # and reserve percentage defined. 7/16/10
         #---------------------------------------------------------------------------
         if ( $digitalReservesTaken and lc $digitalReservesTaken eq 'y' ) {
            if ( !$smsg ) {
               appendString( $errorCode, "Digital reserves without liquidation schedule");
               ++$excount->{digital_reserves_without_schedule};
            }
            if ( !$reservePercentage ) {
               appendString( $errorCode, "Digital reserves without reserve percentage");
               ++$excount->{digital_reserves_without_reserve_percentage};
            }

         }


         #----------------------------
         # Save the reserve percentage
         # TODO: validate
         #----------------------------
         $terms{$priority}->{data}->{reserve_rate} = $reservePercentage if ( $reservePercentage );

         #-----------------------------------------
         #
         # Store successfully validated information
         #
         #-----------------------------------------

         die("_validate: already seen term [$priority]") if ( exists $seenPriorityMap{$priority} );

         $seenPriorityMap{$priority} = 1;


         #$terms{$priority}->{data}->{artist_contract_id} = $contractID
         #   if ( $contractID );


         $terms{$priority}->{data}->{contract_term_source_id} = $contractTermSourceID; # 7/7

         $terms{$priority}->{data}->{income_source_id} = $incomeSourceID
            if ( $incomeSourceID );

         $terms{$priority}->{data}->{region_id} = $regionID
            if ( defined $regionID );

         $terms{$priority}->{data}->{channel_id} = $channelID
            if ( defined $channelID );

         $terms{$priority}->{data}->{price_level_id} = $priceLevelID
            if ( defined $priceLevelID );

         $terms{$priority}->{data}->{contract_rate_type_id} = $contractRateTypeID
            if ( $contractRateTypeID );

         #=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
         #
         # A brief note... you will get duplicate term exceptions if one of
         # the comparison criteria does not apply to the income source.  For
         # example, price level does not apply to digital terms.  If the
         # contract contains two DA terms, with different price levels (say
         # Full vs. Mid), then the 'termKey' below will be the same for both
         # terms since priceLevelID would not have been set in either case.
         #
         # TODO: Add additional exceptions to cover the invalid combinations.
         # -ES 5/5/10
         #=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
         report("WARNING: priceLevelID is not set, src("
            . ( (defined $incomeSourceID) ? $incomeSourceID : "---" )
            . ")") if ( !$priceLevelID );

         #---------------------------------
         # Check if the term is a duplicate
         #---------------------------------
         my $termKey = join("\t",
            printNull($incomeSourceID),
            printNull($regionID),
            printNull($channelID),
            #printNull($priceLevelID),
            #printNull($contractRateTypeID),
            (defined $seenPriceLevelID ? $seenPriceLevelID : 'NULL'),
            (defined $seenContractRateTypeID ? $seenContractRateTypeID : 'NULL'),
         );
         if ( exists $seenTermMap{$termKey} ) {
            my $curRowid = $seenTermMap{$termKey};
            report("  row($rowid): duplicate of term on row($curRowid); key($termKey)");
            appendString( $errorCode, "Duplicate term");
            ++$excount->{duplicate_term};
         } else {
            $seenTermMap{$termKey} = $zzRowid;
         }

         #-----------------------------------------------------
         #
         # If there are _any_ errors, then flag the contract...
         #
         #-----------------------------------------------------
         if ( $errorCode ) {
            $buffer->{$contractKey}->{rows}->{$zzRowid}->{errors} = $errorCode;
            
            $contractHasErrors=1;
         }


         # Next term...
         $ic++;
      }#term validation loop
   }#first pass


   #--------------------------------------------------------------------------------
   # Second validation pass -- this checks for contract-level information which must
   # be consistently defined across all terms.
   #--------------------------------------------------------------------------------
   foreach my $contractKey (keys %$buffer) {

      #my($payeeID,$payorID) = split("-", $contractKey);
      my($payeeID,$payorID) = split("\t", $contractKey); # XXX

      report("### DEBUG_VALIDATION_PASS2: row($contractKey)...");
      #my $terms = $buffer->{$contractKey}->{terms};
      my %terms = %{$buffer->{$contractKey}->{terms}};
      #my $ic=0;
      #report("   There are ".(keys %terms)." term(s):");

      #------------------------------------------------------------------
      # errorCode: this will contain the error message(s) associated with
      # with the term currently being validated.  It will be added to the
      # term's hash using the 'errors' key.
      #------------------------------------------------------------------
      #my $errorCode;

      #---------------------
      # Term validation loop
      #---------------------
      foreach my $priority (keys %terms) {

         my $zzRowid = $terms{$priority}->{rowid};

         my $rowData = $buffer->{$contractKey}->{rows}->{$zzRowid}->{line};

         my($rowid,
            $payeeClientAccount, # 7/17/10
            $payeeName,
            $rsPayeeID,  # 5/5/10
            $contractName,
            $artistName,
            $payorName,
            $issueDate,
            $clientContractID,
            $termStart, # 7/17/10
            $termEnd, # 7/17/10
            $reservePercentage,
            $p1,
            $p2,
            $p3,
            $p4,
            $p5,
            $p6,
            $p7,
            $p8,
            $digitalReservesTaken, # 7/17/10
            $defaultTerm,
            $incomeSource,
            $region,
            $channel,
            $priceTier,
            $rateType,
            $rate,
            $rateReduction,
            $percentOfSales,
            $packaging,
            $freeGoods) = split("\t",$rowData);

         #------------------------------------------------------------------
         # errorCode: this will contain the error message(s) associated with
         # with the term currently being validated.  It will be added to the
         # term's hash using the 'errors' key.
         #------------------------------------------------------------------
         my $errorCode;

         #----------------------------
         # Get any pre-existing errors
         #----------------------------
         if ( exists $buffer->{$contractKey}->{rows}->{$zzRowid}->{errors} ) {
            $errorCode = $buffer->{$contractKey}->{rows}->{$zzRowid}->{errors};
            report("DEBUG: 2ND PASS: rowid($rowid) existing errorCode($errorCode)");
            $contractHasErrors = 1;
         }

         #-------------------------
         # Does the contract exist? 5/11/10
         #-------------------------
         my %cArgs = (
            artist_payee_id => $payeeID,
            payor_id => $payorID,
            title => $contractName,
         );

         if ( $rsPayeeID ) { # override artist payee ID if necessary
            $cArgs{artist_payee_id} = $rsPayeeID if ( $rsPayeeID && '' ne $rsPayeeID );
         }

         $cArgs{client_contract_id} = $clientContractID if ( $clientContractID && '' ne $clientContractID );
         my $cObj = RPS::DB::Item::NewArtistContract->Lookup( %cArgs );
         my $existsMsg = "";
         if ( $cObj ) {
            $contractHasErrors = 1;
            #die("contract exists!!!\n");
            appendString( $errorCode, "Contract exists");
            ++$excount->{contract_exists};

            my $_id = $cObj->artist_contract_id;
            report("DEBUG: contract($_id) exists");
         }


         #-------------------------------------------
         # Check for inconsistent contract name error
         #-------------------------------------------
         if ( scalar keys %contractNameMap > 1 ) {
            appendString( $errorCode, "Contract name not unique");
            ++$excount->{contract_name_not_unique};
         }

         #-------------------------------------------
         # Check for inconsistent artist name error
         #-------------------------------------------
         if ( scalar keys %artistNameMap > 1 ) {
            appendString( $errorCode, "Artist name not unique");
            ++$excount->{artist_name_not_unique};
         }

         #-------------------------------------------
         # Check for inconsistent rsPayeeID
         #-------------------------------------------
         if ( scalar keys %rsPayeeIDMap > 1 ) {
            appendString( $errorCode, "RS payeeID not unique");
            ++$excount->{rs_payee_id_not_unique};
         }

         #----------------------------------
         # Check for inconsistent issue date
         #----------------------------------
         if ( scalar keys %issueDateMap > 1 ) {
            appendString( $errorCode, "Issue date not unique");
            ++$excount->{issue_date_not_unique};
         }

         #----------------------------------
         # Check for inconsistent term start
         #----------------------------------
         if ( scalar keys %termStartMap > 1 ) {
            appendString( $errorCode, "Term start not unique");
            ++$excount->{term_start_not_unique};
         }

         #--------------------------------
         # Check for inconsistent term end
         #--------------------------------
         if ( scalar keys %termEndMap > 1 ) {
            appendString( $errorCode, "Term end not unique");
            ++$excount->{term_end_not_unique};
         }

         #------------------------------------------
         # Check for inconsistent client contract ID
         #------------------------------------------
         if ( scalar keys %clientContractMap > 1 ) {
            appendString( $errorCode, "ContractID not unique");
            ++$excount->{contract_id_not_unique};
         }

         #------------------------------------------
         # Check for inconsistent reserve percentage
         #------------------------------------------
         if ( scalar keys %reservePercentageMap > 1 ) {
            appendString( $errorCode, "Reserve% not unique");
            ++$excount->{reserve_pct_not_unique};
         }

         #--------------------------------------------
         # Check for inconsistent liquidation schedule
         #--------------------------------------------
         if ( scalar keys %scheduleMap > 1 ) {
            appendString( $errorCode, "Reserve schedule not unique");
            ++$excount->{reserve_schedule_not_unique};
         }

#--------------------------------------------
# 7/7/09 - Check for multiple rate reductions
#--------------------------------------------
#if ( scalar keys %rateReductionMap > 1 ) { # TEMPORARY; ONLY FOR UBIQUITY
#   report("WARNING: Non-unique rate reduction rowid($rowid), contract '$contractName'");
#   #appendString( $errorCode, "Non-unique rate reduction");  # allow these to go in
#   ++$excount->{rate_reduction_not_unique};
#}


         if ( $errorCode ) {
            $buffer->{$contractKey}->{rows}->{$zzRowid}->{errors} = $errorCode;
            $contractHasErrors=1;
         }

      }#term loop
   }#2nd validation pass

   #------------------------------------------------------------
   # Final pass: create terms if there were no errors, otherwise
   # dump out the errors.
   #------------------------------------------------------------
   foreach my $contractKey (keys %$buffer) {

      #my($payeeID,$payorID) = split("-", $contractKey);
      my($payeeID,$payorID) = split("\t", $contractKey); # XXX

      report("### DEBUG_2ND_PASS: row($contractKey)...");
      #my $terms = $buffer->{$contractKey}->{terms};
      my %terms = %{$buffer->{$contractKey}->{terms}};
      my %rows = %{$buffer->{$contractKey}->{rows}};

      my $ic=0;
      report("   There are ".(keys %terms)." term(s):");






      if ( $contractHasErrors ) {
         #------------------------------------------------
         # Just dump out the rows and their error messages
         #------------------------------------------------
         foreach my $rowid (sort { $a <=> $b } keys %rows) {
            my $rowData = $buffer->{$contractKey}->{rows}->{$rowid}->{line};

            my $errorCode = "";
            $errorCode = $buffer->{$contractKey}->{rows}->{$rowid}->{errors}
               if ( exists $buffer->{$contractKey}->{rows}->{$rowid}->{errors} );


            _printReportLine($rowData,"__FAIL__", $errorCode);
            #report(join("\t","STATUS",$rowData,"__FAIL__",$errorCode));
         }

      } else {
         #---------------------------------
         # Create the contract if necessary
         #---------------------------------
         my $artistContractID;

         #---------------------
         # Term creation loop
         # Sort the terms based on their rowids...
         #---------------------
         foreach my $priority (sort { $a <=> $b } keys %terms) {
            #my $rowData = $terms{$priority}->{template};
            my $rowid = $terms{$priority}->{rowid};
            my $rowData = $buffer->{$contractKey}->{rows}->{$rowid}->{line};
            
            my $termData = $terms{$priority}->{data};

            #Note: some of the following can be 0, thus the 'defined' check
            #my $contractTermSourceID = $termData->{contract_term_source_id} ?
            #   $termData->{contract_term_source_id} : ""; # 7/7/10
            my $contractTermSourceID = $termData->{contract_term_source_id};


            my $incomeSourceID = $termData->{income_source_id} ? $termData->{income_source_id} : "";
            my $regionID = (defined $termData->{region_id}) ? $termData->{region_id} : "";
            my $channelID = (defined $termData->{channel_id}) ? $termData->{channel_id} : "";
            my $priceLevelID = (defined $termData->{price_level_id}) ? $termData->{price_level_id} : "";
            my $contractRateTypeID = $termData->{contract_rate_type_id} ? $termData->{contract_rate_type_id} : "";
            my $rate = (defined $termData->{rate}) ? $termData->{rate} : "";
            my $issueDate = $termData->{issue_date} ? $termData->{issue_date} : "";

            # termStart / termEnd / digitalReservesEnabled - 7/16/10
            my $termStart = $termData->{term_start} ? $termData->{term_start} : "";
            my $termEnd = $termData->{term_end} ? $termData->{term_end} : "";
            my $digitalReservesEnabled = $termData->{digital_reserves_enabled} ? $termData->{digital_reserves_enabled} : "";

            my $contractName = $termData->{contract_name} ? $termData->{contract_name} : "";
            my $reservePercentage = $termData->{reserve_rate} ? $termData->{reserve_rate} : "";
            my $artistName = $termData->{artist_name} ? $termData->{artist_name} : "";
            my $clientContractID = $termData->{client_contract_id} ? $termData->{client_contract_id} : "";
            my $rateReduction = $termData->{rate_reduction} ? $termData->{rate_reduction} : "";
            my $percentOfSales = $termData->{percent_of_sales} ? $termData->{percent_of_sales} : "";
            my $packaging = $termData->{packaging_deduction} ? $termData->{packaging_deduction} : "";
            my $freeGoods = $termData->{free_goods_deduction} ? $termData->{free_goods_deduction} : "";

            my $p1 = $termData->{p1} ? $termData->{p1} : "";
            my $p2 = $termData->{p2} ? $termData->{p2} : "";
            my $p3 = $termData->{p3} ? $termData->{p3} : "";
            my $p4 = $termData->{p4} ? $termData->{p4} : "";
            my $p5 = $termData->{p5} ? $termData->{p5} : "";
            my $p6 = $termData->{p6} ? $termData->{p6} : "";
            my $p7 = $termData->{p7} ? $termData->{p7} : "";
            my $p8 = $termData->{p8} ? $termData->{p8} : "";
            my $schedKey = join("-", $p1, $p2, $p3, $p4, $p5, $p6, $p7, $p8 );


            my $rsPayeeID = $termData->{rs_payee_id} ? $termData->{rs_payee_id} : "";

            #report("### Creating term pri[$priority]:  src($incomeSourceID) "
            #   . "rg($regionID) ch($channelID) plvl($priceLevelID) "
            #   . "type($contractRateTypeID) rate($rate)");
            report("### Processing term pri[$priority]: " . Dumper(\%$termData));

            #---------------------------------
            # ..create contract if necessary..
            #---------------------------------
            if ( !$artistContractID ) {
               my %cArgs = (
                  artist_payee_id => $payeeID,
                  payor_id => $payorID,
                  title => $contractName,
               );

               if ( $rsPayeeID ) { # override artist payee ID if necessary
                  $cArgs{artist_payee_id} = $rsPayeeID if ( $rsPayeeID && '' ne $rsPayeeID );
               }

               #$cArgs{issue_date} = $issueDate if ( $issueDate && '' ne $issueDate );
               #$cArgs{artist_description} = $artistName if ( $artistName && '' ne $artistName );
               $cArgs{client_contract_id} = $clientContractID if ( $clientContractID && '' ne $clientContractID );
               #$cArgs{reserve_rate} = $reservePercentage if ( $reservePercentage && '' ne $reservePercentage );

               my $cObj = RPS::DB::Item::NewArtistContract->Lookup( %cArgs );
               if ( !$cObj ) {

                  #---------------------------------------------------------------
                  # Issue date, artist description and/or reserve rate should not
                  # be used for lookup purposes, so set them here after the lookup
                  # has been done. 5/11/10
                  #---------------------------------------------------------------
                  $cArgs{issue_date} = date2MySQL($issueDate) if ( $issueDate && '' ne $issueDate );
                  $cArgs{term_start} = date2MySQL($termStart) if ( $termStart && '' ne $termStart );
                  $cArgs{term_end} = date2MySQL($termEnd) if ( $termEnd && '' ne $termEnd );
                  $cArgs{digital_reserves_enabled} = $digitalReservesEnabled if ( $digitalReservesEnabled && '' ne $digitalReservesEnabled );
                  $cArgs{artist_description} = $artistName if ( $artistName && '' ne $artistName );
                  $cArgs{reserve_rate} = $reservePercentage if ( $reservePercentage && '' ne $reservePercentage );

                  if ( $execMode ) {
                     $cObj = RPS::DB::Item::NewArtistContract->Create( %cArgs );
                     $cObj->save();
                     $artistContractID = $cObj->artist_contract_id;
                     $termData->{artist_contract_id} = $artistContractID;
                     report("### Created contract $artistContractID : ". Dumper(\%cArgs));

                     ++$entities->{new_artist_contract};

                     #---------------------------------------
                     # Create the reserve liquidation entries
                     #---------------------------------------
                     _createReserveEntries( \%$entities, $artistContractID, $schedKey );

                  } else {
                     report("WARNING: Non-exec mode, skipped contract creation: ".Dumper(\%cArgs));
                  }
               } else {
                  $artistContractID = $cObj->artist_contract_id;
               }
            }

            next if (!$artistContractID);

            my %ctArgs = (
               artist_contract_id => $artistContractID,
               priority => $priority,
               #contract_term_source_id => $contractTermSourceID, # 7/7/10
            );


            #----------------------------------------------------------------------
            # Recall, we used the incomeSourceID to idenfity "all" source terms via
            # gIncomeSourceMap.  We don't want to actually store those idenfitiers
            # in the database, so we NULL them out prior to creating the term.
            #----------------------------------------------------------------------

            #$ctArgs{income_source_id} = $incomeSourceID if ( $incomeSourceID && '' ne $incomeSourceID);
            if ( $incomeSourceID and $incomeSourceID != 0 and $incomeSourceID != 254 and $incomeSourceID != 255 ) {
               $ctArgs{income_source_id} = $incomeSourceID;
            }

            # contract_term_source_id will be empty for a default term
            $ctArgs{contract_term_source_id} = $contractTermSourceID if ( $contractTermSourceID );


            $ctArgs{region_id} = $regionID if ( _notBlank($regionID) );
            $ctArgs{channel_id} = $channelID if ( _notBlank($channelID) );
            $ctArgs{price_level_id} = $priceLevelID if ( _notBlank($priceLevelID) );
            $ctArgs{contract_rate_type_id} = $contractRateTypeID if ( _notBlank($contractRateTypeID) );
            $ctArgs{rate} = $rate if ( _notBlank($rate) );
            $ctArgs{rate_reduction} = $rateReduction if ( _notBlank($rateReduction) );
            $ctArgs{percentage_of_sales} = $percentOfSales if ( _notBlank($percentOfSales) );
            $ctArgs{packaging_deduction} = $packaging if ( _notBlank($packaging) );
            $ctArgs{free_goods_deduction} = $freeGoods if ( _notBlank($freeGoods) );

            #report("   ## Creating term: ".Dumper(\%ctArgs));

            my $termID=0;
            my $ctObj = RPS::DB::Item::NewArtistContractTerm->Lookup( %ctArgs );
            if ( !$ctObj ) {
               report("DEBUG: schedKey: $schedKey");
               if ( $execMode ) {
                  $ctObj = RPS::DB::Item::NewArtistContractTerm->Create( %ctArgs );
                  $ctObj->save();
                  ++$entities->{new_artist_contract_term};
                  $termID = $ctObj->artist_contract_term_id;
                  $buffer->{$contractKey}->{rows}->{$rowid}->{termid}=$termID;
                  report("   ## Created contract term $termID: ".Dumper(\%ctArgs));

               } else {
                  report("WARNING: Non-exec mode, skipped contract term creation : ".Dumper(\%ctArgs));
               }
            } else {
               # If a term exists, then we should really be catching this _before_ the final
               # validation pass.  If the following occurs, then add in a 3rd pass before
               # the final pass...
               die("ERROR: contract term exists : ".Dumper(\%ctArgs));
            }


            #my($rowid,
            #   $payeeName,
            #   $contractName,
         }#term loop

         #----------------------------------------
         # Dump out each row and its import status
         #----------------------------------------
         foreach my $rowid (sort { $a <=> $b } keys %rows) {
            my $rowData = $buffer->{$contractKey}->{rows}->{$rowid}->{line};
            #my $errorCode = "";
            #$errorCode = $buffer->{$contractKey}->{rows}->{$rowid}->{errors}
            #   if ( exists $buffer->{$contractKey}->{rows}->{$rowid}->{errors} );

            my $termID = 0;
            if ( exists $buffer->{$contractKey}->{rows}->{$rowid}->{termid} ) {
               $termID = $buffer->{$contractKey}->{rows}->{$rowid}->{termid};
            }
            #die("no termID found for rowid($rowid)?!\n") if ( !$termID );

            my $desc = "term($termID)";
            #report(join("\t","STATUS",$rowData,"","term($termID)"));
            _printReportLine($rowData,"", $desc);
         }
      }
   }#contract loop

   #-------------------------------------------------
   # XXX: The following is for debugging purpose only
   #-------------------------------------------------
   report("# of unique contract name(s): ". (scalar keys %contractNameMap));
   report("# of unique artist name(s): ". (scalar keys %artistNameMap));

}#_validate

#sub _termSort {
#   my($a_rowid) = split("\t",$a);
#   my($b_rowid) = split("\t",$b);
#   return $a_rowid <=> $b_rowid;
#}

sub _printReportLine {
   my($rowData,$status,$description) = @_;
   my($rowid,
      $payeeClientAccount,
      $payeeName,
      $rsPayeeID, # 5/5/10
      $contractName,
      $artistName,
      $payorName,
      $issueDate,
      $clientContractID,
      $termStart,
      $termEnd,
      $reservePercentage,
      $p1,
      $p2,
      $p3,
      $p4,
      $p5,
      $p6,
      $p7,
      $p8,
      $digitalReservesTaken,
      $defaultTerm,
      $incomeSource,
      $region,
      $channel,
      $priceTier,
      $rateType,
      $rate,
      $rateReduction,
      $percentOfSales,
      $packaging,
      $freeGoods) = split("\t",$rowData);

   # TODO: order the columns based on gColumnMap
   report(join("\t", "STATUS:",
      $payeeClientAccount,   # A - 7/16/10
      $payeeName,            # B
      $rsPayeeID,            # C - 5/5/10
      $contractName,         # D
      $artistName,           # E
      $payorName,            # F
      $issueDate,            # G
      $clientContractID,     # H
      $termStart,            # I - 7/16/10
      $termEnd,              # J - 7/16/10
      $reservePercentage,    # K
      $p1,                   # L
      $p2,                   # M
      $p3,                   # N
      $p4,                   # O
      $p5,                   # P
      $p6,                   # Q
      $p7,                   # R
      $p8,                   # S
      $digitalReservesTaken, # T
      $defaultTerm,          # U
      $incomeSource,         # V
      $region,               # W
      $channel,              # X
      $priceTier,            # Y
      $rateType,             # Z
      $rate,                 # AA
      $rateReduction,        # AB
      $percentOfSales,       # AC
      $packaging,            # AD
      $freeGoods,            # AE
      $status,               # AF
      $description           # AG
   ));
}

sub _notBlank {
   my($s) = @_;
   return ((defined $s) && '' ne $s);
}

sub _createReserveEntries {
   my ($entities,$contractID, $s) = @_;
   my @values = split("-",$s);
   my $period=1;
   foreach my $value (@values) {
      #report("_createReserveEntries: per($period) = $value");

      if ( '' ne $value ) {
         my %args = (
            id => $contractID,
            type => 3,  # 3 = artist contract
            period => $period,
            percent => $value,
         );
         my $rObj = RPS::DB::Item::ReserveLiquidation->Lookup( %args );
         if ( !$rObj ) {
            if ( $execMode ) {
               $rObj = RPS::DB::Item::ReserveLiquidation->Create( %args );
               $rObj->save();
               ++$entities->{reserve_liquidation};
               report("INFO: created reserve_liquidation entry : " . Dumper(\%args));
            } else {
               report("WARNING: non-exec mode, skipped reserve_liquidation "
                  . "entry for contract $contractID, period $period");
            }
         }
      }
      ++$period;
   }
}#_createReserveEntries

#-------------------------------------------------------------
# Name: _isValidSchedule
# This subroutine checks if a schedule is valid.
# Returns: a string containing the error message(s), undef
#   if no errors were found
#-------------------------------------------------------------
sub _isValidSchedule {
   my ($s) = @_;
   my @values = split("-",$s);
   #my($p1,$p2,$p3,$p4,$p5,$p6,$p7,$p8) = split("-",$s);
   my $e;

   my $m;
   my $total=0;
   my $period=1;
   foreach my $value (@values) {

      if ( '' ne $value ) {
         $m = _isValidPeriod($period,$value);
         appendString( $e, $m ) if ( $m );

         $total += $value;
      }
      ++$period;
   }

  appendString($e,"Schedule over 100%") if ( $total > 100 );


#   my $m = _isValidPeriod(1, $p1);
#   appendString( $e, $m ) if ( $m );
#
#   $m = _isValidPeriod(2, $p2);
#   appendString( $e, $m ) if ( $m );
#
#   $m = _isValidPeriod(3, $p3);
#   appendString( $e, $m ) if ( $m );
#
#   $m = _isValidPeriod(4, $p4);
#   appendString( $e, $m ) if ( $m );
#
#   $m = _isValidPeriod(5, $p5);
#   appendString( $e, $m ) if ( $m );
#
#   $m = _isValidPeriod(6, $p6);
#   appendString( $e, $m ) if ( $m );
#
#   $m = _isValidPeriod(7, $p7);
#   appendString( $e, $m ) if ( $m );
#
#   $m = _isValidPeriod(8, $p8);
#   appendString( $e, $m ) if ( $m );


   return $e;
}#_isValidSchedule

sub _isValidPeriod {
   my($n,$p) = @_;
   my $m;
   if ( $p =~ m/\./ ) {
      $m = "P$n not an integer";
   } elsif( $p =~ m/\D/ ) {
      $m = "P$n non-numeric";
   }
}#_isValidPeriod

#--------------------------------------------------------------
# Name: _isValidDate
# This subroutine checks if a string is in the form YYYY-MM-DD,
# YYYY/MM/DD or \d\d\d\d\d (assumed to be an Excel date code).
# Returns 1 if format is valid, undef otherwise.
#--------------------------------------------------------------
sub _isValidDate {
   my($s) = @_;
   my $retval;
   my $err;
   my $year;
   my $month;
   my $day;

   if ( $s =~ m/(\d+)-(\d+)-(\d+)/ )
   {
      report("DEBUG:_isValidDate: dateString contains hyphens");
      ($year,$month,$day) = split("-",$s);
   }
   elsif ( $s =~ m/(\d+)\/(\d+)\/(\d+)/ )
   {
      report("DEBUG:_isValidDate: dateString contains slashes");
      ($year,$month,$day) = split("/",$s);
   }
   elsif( $s =~ /\d\d\d\d\d/ )
   {
      my $date = Spreadsheet::ParseExcel::Utility::ExcelFmt( "yyyy-mm-dd", $s );
      report("DEBUG:_isValidDate: Excel code dateString parsed as '$date'");
      ($year,$month,$day) = split("-",$date);
   }

   if ( $year ) {
      report("DEBUG:_isValidDate: dateString($s) year($year) month($month) day($day)");
      if ( $year =~ /\D/   ||
           $month =~ /\D/  ||
           $day =~ /\D/ ) {
         report("_isValidDate: non-digit in year/mm/dd");
         $err=1;
      }
      if ( $year < 1900 || $year > 2100 ) {
         report("_isValidDate: year out of bounds");
         $err=1;
      }
      if ( $month < 1 || $month > 12 ) {
         report("_isValidDate: month out of bounds");
         $err=1;
      }
      if ( $day < 1 || $day > 31 ) { # NB: I know we're not checking for things like 6/31 ...
         report("_isValidDate: day out of bounds");
         $err=1;
      }
      $retval = (!$err) ? 1 : undef;
   }
   return $retval;
}

sub _normalizeDate {
   my($dstr) = @_;

   if ( $dstr =~ m/^\d\d\d\d\d$/ ) {
      my $_dt = Spreadsheet::ParseExcel::Utility::ExcelFmt( "yyyy-mm-dd", $dstr );
      report("_normalizeDate:  Converted msft date $dstr to $_dt");
      $dstr = $_dt;
   }
   return $dstr;

}

#------------------------------------------------------------------
# Name: _isPriceLevelAllowed
# This subroutine checks if the specified income source can have an
# adjustable price level.
# Returns 1 if price level can be set for the income source,
# undef otherwise.
#------------------------------------------------------------------
sub _isPriceLevelAllowed {
   my($incomeSourceID) = @_;
   my $retval;
   $retval = 1 if (
      $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceCD ||
      $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceLP ||
      $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceLP5 ||   # NEW - 4/17/10
      $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceCassette ||
      #$incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDigitalAlbum ||
      #$incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDigitalTrack ||
      #$incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDigitalStream ||
      #$incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDigitalTethered ||
      #$incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceRingtone ||
      #$incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceJukebox ||
      #$incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDualDownload ||
      #$incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceBackground ||
      $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDVDCDSet ||
      $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceCDSingle ||
      $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDblCD ||
      #$incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDblVPD ||
      #$incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDigitalAlbumPremium ||
      #$incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDigitalTrackPremium ||
      #$incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDigitalAlbumUpgrade ||
      #$incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDigitalTrackUpgrade ||
      $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDVD ||
      $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceVHS ||
      $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceLP5 ||
      $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceCassette5 ||
      $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceBluRay ||
      $incomeSourceID == 255 ||  # all physical
      $incomeSourceID == 0  # enable price levels for 'all' (a superset of ALL-P)
   );
}#_isPriceLevelAllowed

#--------------------------------------------------------------------------
# Name: _isValidContractRateType
# This subroutine checks if contract rate type is allowed for the specified
# income source.
# Returns 1 if rate type is valid, undef otherwise.
#--------------------------------------------------------------------------
sub _isValidContractRateType {
   my($incomeSourceID, $rateID) = @_;
   my $retval;
   #assert($incomeSourceID);
   assert($rateID);

   if ( $rateID == RPS::DB::Item::ContractRateType::kRateTypeNonPayable ) {

      $retval = 1; # valid for all source types

   } elsif ( $rateID == RPS::DB::Item::ContractRateType::kRateTypePercentRevenue ) {

      $retval = 1; # valid for all source types

   } elsif ( $rateID == RPS::DB::Item::ContractRateType::kRateTypePercentGrossRevenue ) {

      $retval = 1; # valid for all source types

   } elsif ( $rateID == RPS::DB::Item::ContractRateType::kRateTypePercentRetail ||
             $rateID == RPS::DB::Item::ContractRateType::kRateTypePercentWholesale
             ) {
      $retval = 1 if (
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceCD ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceLP ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceLP5 ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceCassette ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDigitalAlbum || # valid as of 8/31/11
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDigitalTrack || # valid as of 8/31/11
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDVDCDSet ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceCDSingle ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDblCD ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDVD ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceVHS ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceCassette5 ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceBluRay ||
         $incomeSourceID == 255 || # all physical
         $incomeSourceID == 0 # all
      );
   } elsif ( $rateID == RPS::DB::Item::ContractRateType::kRateTypePercentDocumentRetail ||   # XXX - Verify
             $rateID == RPS::DB::Item::ContractRateType::kRateTypePercentDocumentWholesale   # XXX - Verify
             ) {
      $retval = 1 if (
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceCD ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceLP ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceLP5 ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceCassette ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDVDCDSet ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceCDSingle ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDblCD ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDVD ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceVHS ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceCassette5 ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceBluRay ||
         $incomeSourceID == 255 || # all physical
         $incomeSourceID == 0 # all
      );
   } elsif ( $rateID == RPS::DB::Item::ContractRateType::kRateTypeFixed ) {
      $retval = 1 if (
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceCD ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceLP ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceLP5 ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceCassette ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDigitalAlbum ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDigitalTrack ||
         #$incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDigitalStream ||
         #$incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDigitalTethered ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceRingtone ||
         #$incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceJukebox ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDualDownload ||
         #$incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceBackground ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDVDCDSet ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceCDSingle ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDblCD ||
         #$incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDblVPD ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDigitalAlbumPremium ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDigitalTrackPremium ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDigitalAlbumUpgrade ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDigitalTrackUpgrade ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDVD ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceVHS ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceCassette5 ||
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceBluRay ||
         $incomeSourceID == 255 || # all physical
         $incomeSourceID == 0 # all
      );
   } 
   return $retval;
}#_isValidContractRateType

# _isPhysical: return 1 if incomeSourceID is physical; undef otherwise
sub _isPhysical {
   my($incomeSourceID) = @_;
   my $retval;
   if ( $incomeSourceID &&
        ( $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceCD ||
          $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceLP ||
          $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceLP5 ||   # 5/11/10
          $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceCassette ||
          $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDVDCDSet ||
          $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceCDSingle ||
          $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDblCD ||
          $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDVD ||
          $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceVHS) )
   {
      $retval = 1;
   }
   return $retval;
}# _isPhysical

sub _validateRegion {
   my($errorCode, $excount) = @_;

}#_validateRegion

1;
