package Support::Implementation::ArtistContractAddTerms;
# 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
# 9/9/10 - Added check for fields that should not be set for
#   non-payable terms.
# 10/6/10 - Fixed gContractRateTypeMap.  Allow percent of sales for all formats.
# 12/22/11 - Adjusted gTemplateHeader to properly recognize 'free goods'.
# 4/2/15 - Updated contract-title and first-last-priority column names
# 4/3/15 - Updated to use _isValidContractRateType, _isPriceLevelAllowed
#   from ArtistContractTemplate
# TODO: Need to check if user is trying to enter non-relevant data
#  for digital terms (e.g., deductions).
#  4/3/15: Need to use more of the validation code from ArtistContractTemplate
#  9/18/15: Allow packaging and free goods to be set on digital terms (FB11304)
#  11/7/16: Bug fixes: fixed rate type, price level and channel handling of 'all'
# 4/14/17: Added Excel 2007 support; changed "free goods" to "free-goods"
# 4/19/17: Added synonym for 'non payable'.  Added PI checks for channel/price tier.
# 4/20/17: Updated exception header from "free goods" to "free-goods"
# 1/31/18: Removed 'all' as a region (caused an issue with FB21916)
# 9/17/19: Added mapping for '% of net revenue'; made contract title searches case-insensitive
# 4/29/20: Limit new_artist_contract search to active (deleted=0) contracts.
# 5/4/20: Allow zero rate for % new revenue
# TODO: Check if zero rate allowable for all terms (not just non-payable and %net revenue)
#
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 clean_name_catalog);

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 );

use Support::Implementation::ArtistContractTemplate;
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 = (
   "*contract-title"        => 0, # A
   "contract-id"           => 1, # B
   "payee-client-account#" => 2, # C
   "payee-name"            => 3, # D
   "rs-contract-id"        => 4, # E
   "*first-last-priority"   => 5, # F
   "source"                => 6, # G
   "region"                => 7, # H
   "channel"               => 8, # I
   "price-type"            => 9, # J
   "rate-type"             => 10, # K
   "rate"                  => 11, # L
   "rate-reduction"        => 12, # M
   "%-of-sales"            => 13, # N
   "packaging"             => 14, # O
   "free-goods"            => 15, # P
);

my %gOptionalColumns;

#--------------------------------------------------------------
# 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;

# gRegionMap -- a map of regionID's to their names
my %gRegionMap;

# gNonUniqueRegionMap -- this is used to keep track of non-unique
# regionIDs.  If a regionID exists in this map then it refers to
# a non-unique region name.
my %gNonUniqueRegionMap;


# 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

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;
#      next if( $clientID == 294 && $name eq 'US' && $id == 50 ); # XXX skip E1's 2nd 'US' entry

      if ( exists $gRegionMap{ lc $name } ) {
         $gRegionMap{ lc $name } .= ",$id";
      } else {
         $gRegionMap{ lc $name } = $id;
      }

   }
#   $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 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 physical' } = 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;
   }

   #-------------------------------
   # 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{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() ) {
      $name =~ s/ //g; # 10/6/10
      $gContractRateTypeMap{ lc $name } = $id;

      # store synonyms that are used by the template specification 1.1
      $gContractRateTypeMap{ retail }    = $id if ( $name =~ m/retail$/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{ "gross revenue" } = $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{ '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{ '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 - 4/17/10 <-- official
      $gContractRateTypeMap{ 'wholesale-document' } = $id if ( $name =~ m/\% Wholesale - Document$/i ); # NEW - 4/17/10

      $gContractRateTypeMap{ '% retail document' } = $id if ( $name =~ /retaildocument$/i ); # 4/19/17
      $gContractRateTypeMap{ '% wholesale document' } = $id if ( $name =~ /wholesaledocument$/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{ '% of net revenue' } = $id if ( $name =~ /netrevenue$/i ); # 9/17/19
      $gContractRateTypeMap{ '%ofnetrevenue' } = $id if ( $name =~ /netrevenue$/i ); # 9/17/19

      $gContractRateTypeMap{ '% gross revenue' } = $id if ( $name =~ /grossrevenue$/i ); 
      $gContractRateTypeMap{ '% of gross revenue' } = $id if ( $name =~ /grossrevenue$/i ); 
      $gContractRateTypeMap{ '%ofgrossrevenue' } = $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/17
   }

   #----------------------------------
   # Build a list of payee information
   #----------------------------------
   $sql = "SELECT artist_payee_id, name FROM artist_payee";
   $sth = $dbo->DoCmd($sql);
   while( my($id,$name) = $sth->fetchrow_array() ) {
      $name =~ s/\s*$//g; # 5/7/10
      $gPayeeIDMap{$id} = $name;

      ++$gPayeeCountMap{$name};
   }


   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;
}

#-----------------------------------------------------------------------
# _validateHeader checks for any missing columns required by the license
# template (we allow 'extra' columns, but the all of the core template
# columns must be present in the header.
# Returns 1 if the header is valid, undef otherwise
#-----------------------------------------------------------------------
sub _validateHeader {
   my $hasErrors;

   #----------------------------------------------------------------------------
   # actualColumns will hold the list of header columns that we actually read in
   #----------------------------------------------------------------------------
   my %actualColumns;
   foreach my $k (keys %gColumnMap) {
      my $columnName = $gColumnMap{$k};
      ++$actualColumns{$columnName};
   }

   foreach my $col (keys %gTemplateHeader) {

      next if ( exists $gOptionalColumns{$col} );

      if ( not exists $actualColumns{$col} ) {
         report("LicenseTemplate:_validateHeader: Missing column '$col'");
         $hasErrors = 1;
      }
   }

   return ($hasErrors) ? undef : 1;


}#_validateHeader

sub printHeader {
   #----------------------------------------------------------------------------
   # 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:",
      "*contract-title",        # A
      "contract-id",           # B
      "payee-client-account#", # C
      "payee-name",            # D
      "rs-contract-id",        # E
      "*first-last-priority",   # F
      "source",                # G
      "region",                # H
      "channel",               # I
      "price-type",            # J
      "rate-type",             # K
      "rate",                  # L
      "rate-reduction",        # M
      "%-of-sales",            # N
      "packaging",             # O
      "free-goods",            # P
      "Error Code",            # Q
      "Description",           # R
   ));
}

#--------------------------------------------------------------
# Read-in all of the template data into memory.  Once it's been
# read, try to parse it and
#--------------------------------------------------------------
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 ) ) {
      print("ArtistContractAddTerms::loadMemory -- loading Excel2k3 $fileName into memory\n");

      #--------------------------
      # Read in the 1st worksheet
      #--------------------------
      my %data;
      my $reader = Support::Implementation::ExcelReader->new(
         filename => $fileName,
         data => \%data,
         header => \%gTemplateHeader,
         columnmap => \%gColumnMap,
         tab => 1,
      );
      $reader->scanExcelFile();

      die("Incorrect header - unable to process template") if ( ! _validateHeader() );

      # Print the report header
      printHeader();

      #---------------------
      # ..Now parse the data
      #---------------------
      _processData(\%data);

      #return %data;
   }
   elsif( $self->isExcel2007( $fileName ) ) {
      print("ArtistContractAddTerms::loadMemory -- loading Excel2k7 $fileName into memory\n");

      #--------------------------
      # Read in the 1st worksheet
      #--------------------------
      my %data;
      my $reader = Support::Implementation::Excel2007Reader->new(
         filename => $fileName,
         data => \%data,
         header => \%gTemplateHeader,
         columnmap => \%gColumnMap,
         tab => 1,
      );
      $reader->scanExcelFile();

      die("Incorrect header - unable to process template") if ( ! _validateHeader() );

      # Print the report header
      printHeader();

      #---------------------
      # ..Now parse the data
      #---------------------
      _processData(\%data);

   } elsif( $self->isTabDelimited( $fileName ) ) {
      #die("Whoa.... I don't know how to do this with tab-delimited files!!!\n");
      report("ArtistContractAddTerms::loadMemory -- processing tab-delimited file");

      my %data;
      my $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("ArtistContractAddTerms -- _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 = ();

   #------------------------------------------------------------------------------
   # contractPriorityMap - this is used to keep track of the first-last-priority
   # setting per contract.  The requested priority must be consistent for all
   # terms in the add terms template.  E.g., all terms being added to a contract
   # must be added first (before the existing priority 1 term), or last (after the
   # last defined term).
   #------------------------------------------------------------------------------
   my %contractPriorityMap = ();
#
#   my @errorList = ();
#
#   #--------------------------------------------
#   # Get the regions associated with this client
#   #--------------------------------------------
#   my $sql = "SELECT region_id, name FROM region";
#   my $sth = $dbo->DoCmd($sql);
#   my @regionList;
#   while( my($regionID, $name) = $sth->fetchrow_array() ) {
#      my $regionInfo = { "region_id" => $regionID, "name" => $name };
#      push @regionList, $regionInfo;
#   }
#
#
#   #--------------------------------------------------------------
#   # If a contract has conflicting terms, then it'll appear in the
#   # following hash.
#   #--------------------------------------------------------------
#   my %contractErrorMap = ();
#
#   #------------------------------------------------------------
#   # Map of rate and rate reduction for each contract by region.
#   #------------------------------------------------------------
#   my %rateMap = ();
#

   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.
   #
   # 7/20/10 - If firstLastPriority is set to 'last', then the current
   # priority will be set to the next available priority, otherwise it will
   # be left at 1 and we'll renumber the existing terms before inserting
   # the new terms.
   #-------------------------------------------------------------------------
   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;

   #---------------------------------------------------------------------------
   # 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 $artistName        = $row->{'artist-name'};
      my $payorName         = $row->{'payor-name'};
      my $issueDate         = $row->{'issue-date'};

      my $termStart         = $row->{'term-start'}; # 7/16/10
      my $termEnd           = $row->{'term-end'}; # 7/16/10
      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'};

      #.....................................................................
      # The above variables are not used, but we need them declared in order
      # to make use of the 'rowData' buffer (which is used by the validation
      # code. -ES 7/20/10
      #.....................................................................

      # IMPORTANT: key names should not have any spaces in them !!! ES 9/18/15

      my $contractName       = $row->{'*contract-title'};        # A - contract-title
      my $clientContractID   = $row->{'contract-id'};           # B
      my $payeeClientAccount = $row->{'payee-client-account#'}; # C

      my $payeeName          = $row->{'payee-name'};            # D
      my $rsPayeeID          = $row->{'rs-payee-id'}; # 5/5/10

      my $rsContractID       = $row->{'rs-contract-id'};        # E
      my $firstLastPriority  = $row->{'*first-last-priority'};   # F
      my $incomeSource       = $row->{'source'};                # G - "income-source"
      my $region             = $row->{'region'};                # H
      my $channel            = $row->{'channel'};               # I
      my $priceTier          = $row->{'price-type'};            # J - "price-tier"
      my $rateType           = $row->{'rate-type'};             # K
      my $rate               = $row->{'rate'};                  # L
      my $rateReduction      = $row->{'rate-reduction'};        # M
      my $percentOfSales     = $row->{'%-of-sales'};            # N - "percent-of-sales"
      my $packaging          = $row->{'packaging'};             # O
      my $freeGoods          = $row->{'free-goods'};            # P - "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 );
      $clientContractID      =~ s/\s*$//g if ( $clientContractID   );

      $incomeSource =~ s/^\s*//g if ( $incomeSource );
      $incomeSource =~ s/\s*$//g if ( $incomeSource );
      $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 );


      $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 || '' eq $rate);
      $rateReduction     = '' if (!$rateReduction);
      $percentOfSales    = '' if (!$percentOfSales);
      $packaging         = '' if (!$packaging);
      $freeGoods         = '' if (!$freeGoods);
      $rsContractID      = '' if (!$rsContractID);


      #---------------------------------------------------
      # For this importer, the contract must already exist
      #---------------------------------------------------


#      #------------------------------
#      # Find the payee (if specified)
#      #------------------------------
##      my $rsPayeeID;
#      if ( $payeeClientAccount or $payeeName ) {
#         my %args;
#         $args{client_account_id} = $payeeClientAccount if ( $payeeClientAccount );
#         $args{name}              = $payeeName if ( $payeeName );
#
#         my $o = RPS::DB::Item::ArtistPayee->Lookup(%args);
#
#         if ( $o ) {
#
#            $rsPayeeID = $o->artist_payee_id;
#
#            report("_processData:DEBUG: payee(" . printNull($payeeName) . ") "
#               . "  payeeClientAccount(".  printNull($payeeClientAccount) . ") "
#               . " --> payeeID($rsPayeeID)");
#         } else {
#
#            report("_processData:DEBUG: payee(" . printNull($payeeName) . ") "
#               . "  payeeClientAccount(".  printNull($payeeClientAccount) . ") "
#               . "not found");
#
#            appendString( $errorCode, "Payee not found");  # moved to _validate
#            ++$excount{payee_not_found};
#            
#         }
#      }
#
#      #------------------
#      # Find the contract
#      #------------------
#      my $artistContractID;
#
#      if ( !$rsContractID ) {
#         if ( $contractName or $clientContractID ) {
#            my %args;
#            $args{title}              = $contractName if ( $contractName );
#            $args{client_contract_id} = $clientContractID if ( $clientContractID );
#            my $o = RPS::DB::Item::NewArtistContract->Lookup( %args );
#            if ( $o ) {
#
#               $artistContractID = $o->artist_contract_id;
#
#               report("_processData:DEBUG: found contractID($artistContractID) : ".Dumper(\%args));
#
#            } else {
#
#               appendString( $errorCode, "Contract not found: ". Dumper(\%args));
#               ++$excount{contract_not_found};
#            }
#
#         } else {
#            appendString( $errorCode, "No contract info specified");
#            ++$excount{missing_contract_info};
#         } 
#      } else {
#
#         my $o = RPS::DB::Item::NewArtistContract->Lookup( artist_contract_id => $rsContractID );
#
#         if ( $o ) {
#
#            my $liveTitle = $o->title;
#            my $liveClientContractID = $o->client_contract_id;
#
#            my $err;
#
#            # If any contract info was specified in template, make sure it's valid
#
#            if ( $clientContractID and $liveClientContractID
#                 and $clientContractID ne $liveClientContractID ) {
#               appendString( $errorCode, "Contract ID doesn't match");
#               ++$excount{contract_id_mismatch};
#               $err = 1;
#            }
#
#            if ( $contractName and $liveTitle
#                 and $contractName ne $liveTitle ) {
#               appendString( $errorCode, "Contract title doesn't match");
#               ++$excount{contract_title_mismatch};
#               $err = 1;
#            }
#
#            $artistContractID = $o->artist_contract_id if ( !$err );
#
#         } else {
#            appendString( $errorCode, "Invalid rs-contract-ID specified");
#            ++$excount{invalid_rscontract_id};
#         }
#
#      }
#
#      #-----------------------------------------
#      # Make sure first/last exists and is valid - 7/16/10
#      #-----------------------------------------
#      if ( $firstLastPriority ) {
#
#         if ( $firstLastPriority !~ m/first/i and
#              $firstLastPriority !~ m/last/i ) {
#
#            appendString( $errorCode, "Invalid priority");
#            ++$excount{invalid_priority};
#         }
#
#      } else {
#         appendString( $errorCode, "Missing priority");
#         ++$excount{missing_priority};
#      }
#
#
#      if ( $artistContractID ) {
#
#         report("_processData:DEBUG: found contractID($artistContractID)");
#
#         #-----------------------------------------------------------
#         # Make sure that the first-last-priority value is consistent
#         # across all terms in the template.
#         #-----------------------------------------------------------
#         if ( not exists $contractPriorityMap{$artistContractID} ) {
#            $contractPriorityMap{$artistContractID} = $firstLastPriority;
#
#            if ( $firstLastPriority and $firstLastPriority =~ /last/i ) {
#
#               # Get the next available priority.  We only do this _once_
#               # per contractID.
#               my $sql = "SELECT MAX(priority)+1 FROM new_artist_contract_term "
#                  . "WHERE artist_contract_id=$artistContractID";
#               my $sth = $dbo->DoCmd($sql);
#               my($_pri) = $sth->fetchrow_array();
#               $currentPriority = $_pri;
#            }
#
#         } else {
#            my $existingPriority = $contractPriorityMap{$artistContractID};
#            if ( lc $existingPriority ne lc $firstLastPriority ) {
#               appendString( $errorCode, "Inconsistent priority");
#               ++$excount{inconsistent_priority};
#            }
#         }
#
#      }


#
# 7/20/10 - given the template, a good chunk of the data in 'rowData' will
# _not_ have any data.  We keep the variables in there because the _validate
# routine (lifted from the ArtistContractTemplate code) assumes that the
# rowData is in a certain format, and I'm trying to avoid mucking with the
# term validation logic as much as possible.  Eventually the term validation
# code should live elsewhere (like in an API) but for now it's essentially
# duplicated in this module along with the artist contract template #1
# importer. -ES
#
      #-------------------------------------------------------------------
      # 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,
         $rsContractID,
         $firstLastPriority,
      );


      report("#### [$rowid]  name($contractName) payee($payeeName) rg($region)"
         . " contractCounter($contractCounter)");


      my $payeeID=0; # payee not specified
      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 finds.  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
#      #-------------------------------------------------------------------
#
      #----------------------
      # Find the artist payee
      #----------------------

      if ( $payeeName || $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 '$payeeName' not found");
            #appendString( $errorCode, "Payee not found");  # moved to _validate
            #++$excount{payee_not_found};

            $payeeID = "NOT_FOUND"; # payee not found

         } else {
            $payeeID = $payeeObj->artist_payee_id;
         }
      }
#
##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("-", $payeeID, $payorID, $contractName );
      my $contractKey = join("\t", $payeeID, $contractName, $clientContractID, $rsContractID, $firstLastPriority ); # 7/20/10 - NB: no payor


#      my $contractKey = $artistContractID; - 7/20/10 - move contract lookup to _validate

#
      if ( '' eq $currentContractKey ) {
         # This is the first term of the first contract in the template
         $currentContractKey = $contractKey;
      }

#
      report("_processData:DEBUG:contractKey($contractKey) currentContractKey($currentContractKey)");

      if ( $currentContractKey eq $contractKey ) {
#
         #-----------------------------------------------------------------
         # Still processing the same contract, so just store the term info.
         #-----------------------------------------------------------------
#
         my $err = _storeTerm(
            $firstLastPriority,
            $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");

         # TODO
#
         #--------------------------------------------------------------------
         #
         # 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;

# 7/20/10 -- allow 'blocks' of terms for a given contract to appear anywhere
# in the template
#         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(
            $firstLastPriority,
            $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 );
   }

#die("ArtistContractAddTerms: STOP!!!");
   #-----------------------
   # 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 ( $firstLastPriority, $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.
         #----------------------------------------------------
         ${_[4]}{rows}->{$rowid}->{line} = $rowData;
         ${_[4]}{rows}->{$rowid}->{errors} = "Duplicate term";

      } else {

         #--------------------------------------------------
         # Store the row data and mark the term as processed
         #--------------------------------------------------
         ${_[4]}{terms}->{0}->{processed} = 1;
         ${_[4]}{rows}->{$rowid}->{line} = $rowData;
         ${_[4]}{terms}->{0}->{rowid} = $rowid;
         ${_[4]}{terms}->{0}->{first_last} = $firstLastPriority; # 7/20/10
         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;
         ${_[4]}{rows}->{$rowid}->{line} = $rowData;
         ${_[4]}{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};
         ${_[4]}{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;
         ${_[4]}{rows}->{$rowid}->{line} = $rowData;
         ${_[4]}{terms}->{$pri}->{processed} = 1;
         ${_[4]}{terms}->{$pri}->{rowid} = $rowid;
         ${_[4]}{terms}->{$pri}->{first_last} = $firstLastPriority; # 7/20/10

         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, $contractName, $clientContractID, $rsContractID, $firstLastPriority) = split("\t", $contractKey); # 7/20/10

#
#      #------------------------------------------------------------
#      # 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
      #-------------------------------
# 7/22/10 -- Moved payee validation below
#      if ( !$rsContractID and (!$payeeID or $payeeID == 0) ) {
#         appendString( $globalErrorCode, "Payee not found");
#         ++$excount->{payee_not_found};
#      }

      # Validate the contract information

      #------------------
      # Find the contract
      #------------------
      my $artistContractID;

      if ( !$rsContractID ) {
         if ( $payeeID ) {

            if ( "NOT_FOUND" ne $payeeID ) {

               if ($contractName || $clientContractID) {
                  my %args = (
                     artist_payee_id => $payeeID,
                  );
                  $args{title}              = $contractName if ( $contractName );
                  $args{client_contract_id} = $clientContractID if ( $clientContractID );
                  $args{deleted}            = 0;
                  my $o = RPS::DB::Item::NewArtistContract->Lookup( %args );
                  if ( $o ) {

                     $artistContractID = $o->artist_contract_id;

                     report("_processData:DEBUG: found contractID($artistContractID) : ".Dumper(\%args));

                  } else {

                     report( "_validate:DEBUG:First loop:Contract not found: ". Dumper(\%args));
                     #appendString( $globalErrorCode, "Contract not found");
                     #++$excount->{contract_not_found};
                     appendString( $globalErrorCode, "Invalid contract");
                     ++$excount->{invalid_contract};
                  }

               } else {
                  appendString( $globalErrorCode, "No contract info specified");
                  ++$excount->{missing_contract_info};
               } 

            } else {

               # Note: we can't catch the case where the user enters a payee clientAccountID that
               # doesn't match the payee name.  These all just drop into the 'invalid payee'
               # bucket.
               appendString( $globalErrorCode, "Invalid payee");
               ++$excount->{invalid_payee};

            }

         } else {
            #
            # No payeeID -- can we uniquely find a contract given the contract
            # title and/or client contract ID?  If so, then it's OK to not have
            # any payee info.  Otherwise, generate a no payee exception.

            if ($contractName || $clientContractID) {
               my $sql = "SELECT artist_contract_id FROM new_artist_contract ";
               my $whereClause;

               if ( $clientContractID ) {
                  if ( $whereClause ) {
                     $whereClause .= "AND client_contract_id='$clientContractID' ";
                  } else {
                     $whereClause .= "WHERE client_contract_id='$clientContractID' ";
                  }
               }

               if ( $contractName ) {
                  if ( $whereClause ) {
                     $whereClause .= "AND title='$contractName' ";
                  } else {
                     $whereClause .= "WHERE title='$contractName' ";
                  }
               }

               $sql .= $whereClause;

               my $sth = $dbo->DoCmd($sql);

               if ( $sth->rows == 1 ) {
                  ($artistContractID) = $sth->fetchrow_array();
                  report("_processData:DEBUG: no payee, but found contractID($artistContractID)");

               } elsif( $sth->rows == 0 ) {

                  report( "_validate:DEBUG:First loop, no payee:Contract not found, "
                     ."title(". printNull($contractName) . ") "
                     ."clientContractID(". printNull($clientContractID) . ") ");

                  appendString( $globalErrorCode, "Contract not found");
                  ++$excount->{contract_not_found};
               } else {

                  
                  report("_processData:DEBUG: no payee, found multiple contracts? "
                     ."title(". printNull($contractName) . ") "
                     ."clientContractID(". printNull($clientContractID) . ")\nSQL = $sql");

                  appendString( $globalErrorCode, "Contract not unique");
                  ++$excount->{contract_not_unique};
               }


            } else {
#               # payee not found (already flagged above)
               appendString( $globalErrorCode, "Payee not found");
               ++$excount->{payee_not_found};
            } 

         }
      } else {

         my $o = RPS::DB::Item::NewArtistContract->Lookup( artist_contract_id => $rsContractID );

         if ( $o ) {

            my $liveTitle = $o->title;
            my $liveClientContractID = $o->client_contract_id;

            my $err;

            # If any contract info was specified in template, make sure it's valid

            if ( $clientContractID and $liveClientContractID
                 and $clientContractID ne $liveClientContractID ) {
               appendString( $globalErrorCode, "Contract ID doesn't match");
               ++$excount->{contract_id_mismatch};
               $err = 1;
            }

            #if ( $contractName and $liveTitle
            #     and (lc $contractName ne lc $liveTitle ) ) {

            my $c1 = lc clean_name_catalog($contractName);
            my $c2 = lc clean_name_catalog($liveTitle);
            if ( $contractName and $liveTitle
                 and ($c1 ne $c2) ) {
               print "ERROR: template title ($c1) doesn't match DB title ($c2)\n";
               appendString( $globalErrorCode, "Contract title doesn't match");
               ++$excount->{contract_title_mismatch};
               $err = 1;
            }

            $artistContractID = $o->artist_contract_id if ( !$err );

         } else {
            report("_processData:WARNING: contractID($rsContractID) is invalid");
            appendString( $globalErrorCode, "Invalid rs-contract-ID specified");
            ++$excount->{invalid_rscontract_id};
         }
      }


      if ( $artistContractID ) {
         #%{$buffer->{$contractKey}->{terms}};
         $buffer->{$contractKey}->{artist_contract_id} = $artistContractID;
      } else {

         report("_processData:DEBUG: No contract found for contractKey($contractKey)");

      }



#
## 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;

      #---------------------------------------------------------------------
      # existingTermMap: This keeps track of the terms that already exist on
      # the contract. -ES 7/20/10
      #---------------------------------------------------------------------
      my %existingTermMap;

      #---------------------
      # 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,
            $_rsContractID,      # new - 7/20/10
            $_firstLastPriority, # new - 7/20/10
            ) = 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) = $gPayeeIDMap{$payeeID};
#            if ( $_payeeName and $gPayeeCountMap{$_payeeName} > 1 ) {
#               appendString( $errorCode, "Duplicate payee name");
#               ++$excount->{duplicate_payee_name};
#            }
#         } else {
#            my ($actualPayeeName) = $gPayeeIDMap{$rsPayeeID};
#            if ( !$actualPayeeName or ( $actualPayeeName ne $payeeName ) ) {
#               appendString( $errorCode, "Payee name mismatch");
#               ++$excount->{payee_name_mismatch};
#            }
#         }


#         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 $seenContractRateTypeID;
         my $seenPriceLevelID;

         my $contractTermSourceID;       # NEW 7/7/10
         my $parentContractTermSourceID; # NEW 7/7/10 (unused for now)

	 # 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) );
	    }
	 }

         if ( "0" ne $priority ) {
            #-----------------
            # Non-default term
            #-----------------

            #--------------------
            # Validate the region
            #--------------------
            if ( '' ne $region ) {
               if ( exists $gRegionMap{lc $region} ) {
                  $regionID = $gRegionMap{ lc $region };

                  if ( $regionID =~ m/,/ ) {   # 7/22/10
                     appendString( $errorCode, "Non-unique region name");
                     ++$excount->{nonunique_region};
                     report("NON_UNIQUE($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
            #---------------------------
#print STDERR "D: looking at incomeSource '$incomeSource'\n"; # XXX
            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 "D: gContractTermSourceMap incomeSource('$incomeSource') --> " .
#   "formatID($formatID) incomeSourceID($incomeSourceID) " .
#   "contractTermSource($contractTermSourceID) " .
#   "parentContractTermSource($parentContractTermSourceID) " .
#   " \n"; # XXX

                  assert($contractTermSourceID);

               } else {
print STDERR "D: invalid incomeSource '$incomeSource'\n"; # XXX
                  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 ( defined $incomeSourceID ) {
		     # TODO: 4/14/17: Need to update _isValidContractRateType to be 'PI' aware
                     if ( Support::Implementation::ArtistContractTemplate::_isValidContractRateType($incomeSourceID, $zzRateID) ) {
                        $contractRateTypeID = $zzRateID;
                     } else {
                        report("D: INFO: invalid rate type($zzRateID) for source($incomeSourceID)"); # XXX
                        appendString( $errorCode, "Invalid rate type for source");
                        ++$excount->{invalid_ratetype_for_source};
                     }
                  }

                  #-----------------------------------------------
                  # 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
            # (formatID 1), all formats (formatID 0) or PI
            # (formatID 3).  We'll only validate if term is payable.
            #-----------------------------------------------------
            if ( $contractRateTypeID && $contractRateTypeID != RPS::DB::Item::ContractRateType::kRateTypeNonPayable ) {
               if ( defined $formatID && ($formatID == 1 || $formatID == 0 ) ) {

                  print "WARNING: incomeSourceID not defined?!\n" if (!defined $incomeSourceID);

                  #-----------------
                  # Validate channel
                  #-----------------
                  if ( '' ne $channel && Support::Implementation::ArtistContractTemplate::_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 && Support::Implementation::ArtistContractTemplate::_isPriceLevelAllowed($incomeSourceID) ) {

                     if ( exists $gPriceLevelMap{lc $priceTier} ) {
                        $priceLevelID = $gPriceLevelMap{ lc $priceTier };
                     } else {
                        appendString( $errorCode, "Unknown price level");
                        ++$excount->{unknown_pricelevel};
                     }
                  } else {
                     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;

                  # TODO: Validate the freeGoods
                  #$terms{$priority}->{data}->{free_goods_deduction} = $freeGoods;

               } elsif( $formatID && $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 && 'all' ne (lc $priceTier) ) { # 5/5/10
                     appendString( $errorCode, "Price level can't be defined for this source");
                     ++$excount->{price_level_not_allowed_on_digital_source};
                  }

                  if ( '' ne $channel && 'all' ne (lc $channel) ) { # 5/5/10
                     appendString( $errorCode, "Channel can't be defined for this source");
                     ++$excount->{channel_not_allowed_on_digital_source};
                  }

               } elsif( $formatID && $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...
               }
            } # end of payable-term format validation

            # Following are allowed for digital or physical terms
            #

            # TODO: Validate the percentOfSales
            $terms{$priority}->{data}->{percent_of_sales} = $percentOfSales;

            # TODO: Validate the packaging
            $terms{$priority}->{data}->{packaging_deduction} = $packaging;

            # 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 && $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 && !$channelID &&
                    !$priceLevelID && !$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 ||
                     $contractTermSourceID == 3 ) { # 'all' (1) or 'all digital' (3)

               # If digital, then we must have a region and rate type

               if ( !$regionID && !$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};
               }
            }


            #------------------------------------------------------------
            # Check if the user is specifying things which don't apply to
            # non-payable terms. -ES 9/9/10
            #------------------------------------------------------------
            if ( defined $rate && 0 != $rate ) {
               report("Rate not applicable to non-payable term");
               appendString( $errorCode, "Rate not applicable to non-payable term" );
               ++$excount->{nonpayable_term_with_rate};
            }

            if ( defined $rateReduction && '' ne $rateReduction ) {
               report("Rate reduction not applicable to non-payable term");
               appendString( $errorCode, "Rate reduction not applicable to non-payable term" );
               ++$excount->{nonpayable_term_with_rate_reduction};
            }
            
            if ( defined $percentOfSales && '' ne $percentOfSales ) {
               report("Percent of sales not applicable to non-payable term");
               appendString( $errorCode, "Percent of sales not applicable to non-payable term" );
               ++$excount->{nonpayable_term_with_percent_of_sales};
            }

            if ( defined $packaging && '' ne $packaging ) {
               report("Packaging not applicable to non-payable term");
               appendString( $errorCode, "Packaging not applicable to non-payable term" );
               ++$excount->{nonpayable_term_with_packaging};
            }

            if ( defined $freeGoods && '' ne $freeGoods ) {
               report("Free goods not applicable to non-payable term");
               appendString( $errorCode, "Free goods not applicable to non-payable term" );
               ++$excount->{nonpayable_term_with_free_goods};
            }
         }


         #==========================
         #
         # 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 ) {
               if ( $contractRateTypeID &&
                    $contractRateTypeID != RPS::DB::Item::ContractRateType::kRateTypeNonPayable &&
                    $contractRateTypeID != RPS::DB::Item::ContractRateType::kRateTypePercentRevenue ) {
                  report("  row($rowid): zero rate: '$rate'\n");
                  appendString( $errorCode, "Zero rate");
                  ++$excount->{zero_rate};
               } else {
                  $terms{$priority}->{data}->{rate} = $rate;
               }

            } else {
               if ( $contractRateTypeID &&
                    $contractRateTypeID == RPS::DB::Item::ContractRateType::kRateTypeNonPayable &&
                    $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 ) {
            if ( !$rsContractID ) {
               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) ) {
               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) ) {
               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{""} = "";
         }


# 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($contractTermSourceID),
            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;
         }


         #----------------------------------------------------------------
         # 7/20/10 - Also check if the term already exists on the contract
         #----------------------------------------------------------------


         #----------------------------------------------------
         # .. existingTermMap is initialized once per contract
         #----------------------------------------------------
#         if ( not %existingTermMap && defined $artistContractID ) {
         if ( ! %existingTermMap && defined $artistContractID ) {
            my $sql = "SELECT priority, "
               . "income_source_id, "
               . "contract_term_source_id, "
               . "region_id, "
               . "channel_id, "
               . "price_level_id, "
               . "contract_rate_type_id "
               . "FROM new_artist_contract_term WHERE artist_contract_id=$artistContractID";
            my $sth = $dbo->DoCmd($sql);
            while( my($_priority, $_incomeSourceID, $_contractTermSourceID,
                     $_regionID, $_channelID, $_priceLevelID,
                     $_contractRateTypeID) = $sth->fetchrow_array() ) {
               
               # Note: contract_term_source_id is what the UI uses..
               #
               my $termKey = join("\t",
                  #printNull($_incomeSourceID),
                  printNull($_contractTermSourceID),
                  printNull($_regionID),
                  printNull($_channelID),
                  printNull($_priceLevelID),
                  printNull($_contractRateTypeID),
               );

               $existingTermMap{$termKey} = $_priority;

            }
         }

         #------------------------------------------------------------------
         # .. check if the term from the template is already on the contract
         #------------------------------------------------------------------
         if ( exists $existingTermMap{$termKey} ) {
            my $existingPriority = $existingTermMap{$termKey};
            report("  row($rowid): duplicate of existing term priority($existingPriority)");
            appendString( $errorCode, "Term exists");
            ++$excount->{term_exists};
         }








         #-----------------------------------------------------
         #
         # 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

report("_validate:DEBUG -- end of first pass: ". Dumper(\%$buffer));


   #------------------------------------------------------------
   # 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, $contractName, $clientContractID, $rsContractID, $firstLastPriority) = split("\t", $contractKey); # 7/20/10

      my $artistContractID;
      if ( $buffer->{$contractKey}->{artist_contract_id} ) {
         $artistContractID = $buffer->{$contractKey}->{artist_contract_id};
      }

      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
         #---------------------------------
# Commented-out following line 7/20/10
#         my $artistContractID;
         #my $artistContractID = $contractKey; # 7/20/10




         #================================================================
         # 7/20/10 - If a priority 1 term exists in the term buffer, then
         # we must renumber the contract's existing terms to make room for
         # the new terms, otherwise we'll end up with duplicate priorities
         # within the contract.
         #================================================================
# Based on conversation w/ CB, each term will be considered
# individually.  E.g., terms within a group can be inserted at the
# top or bottom, depending on whether 'first' or 'last' is in the
# template.  Thus, we we need to evaluate the existing term priorities
# while we are processing a new term from the template.  Soooo this means
# that any term renumbering occurs below, inside the term creation loop.
#

# Because the contractKey contains the firstLast information, we know
# that the first_last field for all terms in the current contract will
# be the same.

         # priorityOffset -- each term in the contract buffer is assigned
         # a relative priority (basically this is the order that have within
         # the template) starting at priority = 1.  The terms that we add
         # will be either be added before existing terms (the new terms will
         # start with priority=1), or added to the end of the existing terms
         # (the new terms will start with currentMaxPriority + the relative
         # priority from the template).  priorityOffset is used to ensure
         # that 'last' terms are added to the end of existing terms.
         my $priorityOffset = 0;

         #if ( exists $terms{1} ) 
         if ( $firstLastPriority =~ m/first/i ) {

            # First, how many terms are we going to be inserting?
            my $numTerms = (keys %terms);
            assert($numTerms);

            if ( $execMode ) {
               my $sql = "UPDATE new_artist_contract_term "
                  . "SET priority = priority + $numTerms "
                  . "WHERE artist_contract_id = $artistContractID "
                  . "AND priority > 0 "
                  ;
               my $sth = $dbo->DoCmd($sql);
               report("_validate:DEBUG: renumbered existing terms on contractID($artistContractID)");
            } else {
               report("_validate:DEBUG: Non-exec mode: skipped renumbering of existing terms on contractID($artistContractID)");
            }

            #die("_validate:final loop: detected priority 1 term! numTerms($numTerms)");

         } elsif ( $firstLastPriority =~ m/last/i ) {
               my $sql = "SELECT MAX(priority) FROM new_artist_contract_term "
                  . "WHERE artist_contract_id = $artistContractID "
                  ;
               my $sth = $dbo->DoCmd($sql);
               ($priorityOffset) = $sth->fetchrow_array();
               report("_validate:DEBUG: LAST -- priorityOffset($priorityOffset) contractID($artistContractID)");
         } else {
            die("_validate:ERROR:final loop -- illegal first/last priority!!! --> $firstLastPriority ");
         }








         #---------------------
         # Term creation loop
         # Sort the terms based on their rowids...
         #---------------------

         # 7/21/10 - The terms are actually sorted on their order within the
         # template, not the rowid from which they originated (if you did
         # sort on the rowid you'd get the same thing).

         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      = exists $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] from rowid($rowid): " . Dumper(\%$termData));

            #---------------------------------
            # ..create contract if necessary..
            #---------------------------------
# Commented-out 7/20/10
#            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} = $issueDate if ( $issueDate && '' ne $issueDate );
#                  $cArgs{term_start} = $termStart if ( $termStart && '' ne $termStart );
#                  $cArgs{term_end} = $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,
               priority => ($priority + $priorityOffset),  # 7/20/10
               #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 && $incomeSourceID != 0 && $incomeSourceID != 254 && $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));

#die("_validate:DEBUG: end of _validate". Dumper(\%$buffer));
}#_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,
      $rsContractID,      # new 7/20/10
      $firstLastPriority, # new 7/20/10
      ) = 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
   #));

   # printLine for AddTerms template
   report(join("\t", "STATUS:",
      $contractName,         # A
      $clientContractID,     # B
      $payeeClientAccount,   # C
      $payeeName,            # D
      $rsContractID,         # E
      $firstLastPriority,    # F

      $incomeSource,         # G
      $region,               # H
      $channel,              # I
      $priceTier,            # J
      $rateType,             # K
      $rate,                 # L
      $rateReduction,        # M
      $percentOfSales,       # N
      $packaging,            # O
      $freeGoods,            # P
      $status,               # Q
      $description           # R
   ));

}

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
# 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);
   }

   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;
}

#------------------------------------------------------------------
# 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 OBE_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 == 255  # all physical
   );
}#_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 OBE_isValidContractRateType { # XXX - Use method in Support::Implementation::ArtistContractTemplate
   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::kRateTypePercentRetail ||
             $rateID == RPS::DB::Item::ContractRateType::kRateTypePercentWholesale ||
             $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 || # XXX - Verify
         $incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceCassette ||
         #$incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDigitalAlbum ||
         #$incomeSourceID == RPS::DB::Item::IncomeSource::kIncomeSourceDigitalTrack ||
         $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 == 255 # all physical
      );
   } 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 || # XXX - Verify
         $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 == 255 # all physical
      );
   } 
   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;
