package Support::Implementation::LicenseTemplate;
#
# 11/13/09 -- Added support for All Digital (ALL-D) and DT product types.
# 3/12/10 -- Changed 'D' to 'DA' in _getProductTypeID()
# 4/8/10 -- Added 'royaltyshare-track-id' support.
# 7/26/10 -- Added support for Ringtone licenses per FB 12173.
# 8/17/10 -- Added code to remove trailing space from track.title_clean
#      for EGR track searches.
# 11/3/10 -- Added validation check for fractional share
# 11/15/10 -- Removed fractional share validation check (these are ok!)
# 11/16/10 -- "All Digital" and "All Physical" are not valid product types.
# 11/29/10 -- Added share range check.
# 1/12/11 -- Added missing rateTypeID check
# 1/20/11 -- Added special handling for track lookups for VP (FB13163)
#  -- Added code to deal with Excel dates
# 2/15/11 -- Added check for "rate too small".
# 2/18/11 -- Added LP5 config to list of product types.
#  -- Changed existing license messaging for PD licenses
# 4/4/11 -- Added reserve total check
# 6/23/11 -- Added RT alias for Ring product type
# 6/29/11 -- Added rate% check
# 9/20/11 -- Updated ringtone validation
# 11/1/11 -- Additional RS fixes
# 11/4/11 -- Fixed percentOfSales logic found while testing 14736
# 12/20/11 -- Added lower bound check for rate percentage (a too small
#   value caused a run to die (Case 15279)).
# 2/23/12 -- Cleaned up productType with trailing spaces
# 3/23/12 -- Cleaned up rateBasis check
# 5/17/12 -- Fixed bug in findTrack (TODO: clean this function up)
# 8/30/12 -- Look for album title using both the title and/or title_clean
#   this is to accomodate clients whose clean_name isn't correct.
# 9/19/12 -- Removed upper bound check on rate percentage
# 9/20/12 -- Use clean_name_catalog() instead of clean()
# 1/2/13 -- Added validation logic to detect region errors
# 1/15/13 -- Updated validation for licenses w/ term data
# 1/16/13 -- Adjusted track search criteria in _findTrack
# 8/12/13 -- Added check for malformed share (FB608; shares had two decimal points)
# 1/27/14 -- Added Excel 2007 support
# 2/19/14 -- Added check for balance formatting; debug code for balance logic
# 2/9/15 -- License balances not working, plus SFW (Smithsonian) requires some
#          special logic to match their albums/tracks.
# 2/10/15 -- More album/track logic tweaking for SFW.  License balance should be ok.
# 3/26/15 -- Updated to allow MM-DD-YYYY (in addition to YYYY-MM-DD)
# 4/2/15 -- Added check to _validateHeader to see if the template is for this importer
# 5/7/15 -- Updated _normalizeDate logic to deal with Excel dates
# 6/11/15 -- Error out if the date isn't valid YYYY-MM-DD format
# 7/20/15 -- Accept additional rate type of 'S' for RING licenses
# 7/22/15 -- Accept additional rate type of 'F' for RING licenses
# 2/19/16 -- Remove CRLF from payor name
# 3/11/16 -- Remove CTRL-M from payor name
# 5/9/16 -- Store normalized dates back in the row hash
#  TODO: Need to store normalized dates in $rows, so that the dates look OK in the
#  exceptions report (especially when the dates are in Excel format)
# 5/11/16 -- Update stat rate parsing to prevent ringtone rates on non-ringtone licenses
# 9/12/16 -- Output track mech exempt message in both exec and non-exec modes.
# 11/8/16 -- Treat 'All Products' as 'All'
#
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 qw(Add_Delta_Days Days_in_Month);

use Spreadsheet::ParseExcel;

use Common::Assert;
use Common::UTF8;
use Common::RSApp;
use Common::Util qw( clean_name_catalog trimspaces);

use RPS::License::BaseTrackLicense;
use RPS::License::US::PublicDomain;
use RPS::License::US::TrackLicense;
use RPS::License::US::RingtoneLicense;
use RPS::License::US::TrackLicenseWithLiquidationSchedule;

use RPS::DB::Item::Album;
use RPS::DB::Item::Track;
use RPS::DB::Item::Master;
use RPS::DB::Item::TrackLicense;
use RPS::DB::Item::Publisher;
use RPS::DB::Item::Payor;
use RPS::DB::Item::Region;
use RPS::DB::Item::RegionCountryMap;
use RPS::DB::Item::Product;
use RPS::DB::Item::ProductType;
use RPS::DB::Item::PendingTransaction;
use RPS::Finance::PendingTransaction;
use RPS::DB::Item::FinanceAccount;
use RPS::Finance::Account;

use RPS::DB::Item::RegionCountryMap;

use Support::Implementation::ExcelReader;
use Support::Implementation::Excel2007Reader;
use Support::Implementation::TabDelimitedReader;
#use Support::Implementation::SearchUtil;

use base 'Support::Implementation::Template';

use constant kQuiet  => 0;
use constant kNormal => 1;
use constant kDebug  => 3;
my $gReportLevel = kNormal;

use constant kAlbumNotFound       => 100;
use constant kTrackNotFound       => 101;
use constant kPublisherNotFound   => 102;
use constant kNonUniqueAlbumName  => 103;
use constant kDuplicateTrackTitle => 104;
use constant kProductTypeNotFound => 99999;

binmode STDOUT, ":utf8";
#-----------------------------------------------------------------------
# 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
#-----------------------------------------------------------------------
my %gTemplateHeader = (
   "album-name" => 0,                  # A
   "catalog-#" => 1,                   # B
   "track-name" => 2,                  # C
   "isrc" => 3,                        # D
   "track-mech-exempt" => 4,           # E
   "public-domain" => 5,               # F
   "product-type" => 6,                # G
   "region" => 7,                      # H
   "share-%" => 8,                     # I
   "payor" => 9,                       # J
   "royaltyshare-publisher-id#" => 10, # K
   "publisher" => 11,                  # L
   "publisher-direct" => 12,           # M
   "issuer-license-id" => 13,          # N
   "status" => 14,                     # O
   "crossed" => 15,                    # P
   "license-date-sent" => 16,          # Q
   "license-date-received" => 17,      # R
   "license-date-issued" => 18,        # S
   "license-term-start" => 19,         # T
   "license-term-end" => 20,           # U
   "rate-type" => 21,                  # V
   "rate-%" => 22,                     # W
   "rate-basis" => 23,                 # X
   "lock-date" => 24,                  # Y
   "penny-rate" => 25,                 # Z
   "reserve-percent" => 26,            # AA
   "p1" => 27,                         # AB
   "p2" => 28,                         # AC
   "p3" => 29,                         # AD
   "p4" => 30,                         # AE
   "p5" => 31,                         # AF
   "p6" => 32,                         # AG
   "p7" => 33,                         # AH
   "p8" => 34,                         # AI
   "percent-of-sales" => 35,           # AJ
   "packaging" => 36,                  # AK
   "free-goods" => 37,                 # AL
   "misc-deduction" => 38,             # AM
   "comments" => 39,                   # AN
   "license-opening-balance" => 40,    # AO
   "royaltyshare-track-id" => 41,      # AP
   "royaltyshare-album-id" => 42,      # AQ
);

#-----------------------------------------------------------------------
# gOptionalColumns -- these columns are optional (e.g., they can be left
# off the template)
#-----------------------------------------------------------------------
my %gOptionalColumns = (
   "royaltyshare-track-id" => 1,
   "royaltyshare-album-id" => 1,
   "packaging" => 1,
);

#--------------------------------------------------------------
# Maps column #'s to a unique key (headername).  The reverse of
# templateHeader, but with actual column #.
#--------------------------------------------------------------
my %gColumnMap = ();

#--------------------------------------------------------
# gHeaderDisplayed is a flag that we set if we've already
# displayed the template header during an exceptions dump
#--------------------------------------------------------
my $gHeaderDisplayed;

# Reference to SearchUtil object
#my $gSearchObj;

my $clientID;
my $execMode;

my $dbo;

sub new {
   my ($class, %args) = @_;
   my $self = bless {}, $class;
   return $self->_init(%args);
}


sub _init {
   my( $self, %args ) = @_;

   report("LicenseTemplate::_init -- args = ". Dumper(\%args));

   $self->SUPER::_init(%args);

   $execMode = $self->exec_mode if ( $self->exec_mode );

   return $self;
}

sub parseHeader {
   my $self = shift;
}

#--------------------------------------------------------------
# Read-in all of the template data into memory.  Once it's been
# read, try to parse it and create track licenses.
#--------------------------------------------------------------
sub loadMemory {
   my $self = shift;

   $clientID = $self->client_id;
   my $app = Common::RSApp->new(clientID => $clientID);

   $dbo = Common::RSApp::GetClientDB();

   #$gSearchObj = Support::Implementation::SearchUtil->new(
   #   clientID => $clientID,
   #);

   my $fileName = $self->name;

   if( $self->isExcel2003( $fileName ) ) {
      print("LicenseTemplate::loadMemory -- loading Excel2k3 $fileName into memory\n");

      # Read in the header
      my %data;
      my $reader = Support::Implementation::ExcelReader->new(
         filename => $fileName,
         data => \%data,
         header => \%gTemplateHeader,
         columnmap => \%gColumnMap
      );
      $reader->scanExcelFile;

      die("Unable to import license template") if (! _validateHeader() );

      #die("loadMemory: ". Dumper(\%gColumnMap) );

      # Try and parse it...
      _processData(\%data);

   } elsif( $self->isExcel2007( $fileName ) ) {
      print("LicenseTemplate::loadMemory -- loading Excel2k7 $fileName into memory\n");

      # Read in the header
      my %data;
      my $reader = Support::Implementation::Excel2007Reader->new(
         filename => $fileName,
         data => \%data,
         header => \%gTemplateHeader,
         columnmap => \%gColumnMap
      );
      $reader->scanExcelFile;

      die("Unable to import license template") if (! _validateHeader() );

      #die("loadMemory: ". Dumper(\%gColumnMap) );

      # Try and parse it...
      _processData(\%data);

   } elsif( $self->isTabDelimited( $fileName ) ) {
      report("LicenseTemplate::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);
   }
}

#-----------------------------------------------------------------------
# _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};
   }

   if( exists $actualColumns{ 'liquidated-units-sales-date' } )
   {
      die("ERROR: You must use the v2 license template to import this file (FB5324)");
   }

   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

#-------------------------------------------------------------------
# _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 license
# data.
#-------------------------------------------------------------------
sub _processData {
   my($data) = @_;
   my $rows = $data->{rows};

   #my @errorList = ();

   my $numNewLicenses = 0;
   my $numExceptions = 0;
   my $numTrackExceptions = 0;
   my $numAlbumExceptions = 0;
   my $numLockDateExceptions = 0;
   my $numPublisherNotFoundExceptions = 0;
   my $numPublisherMismatchExceptions = 0;
   my $numRateTypeExceptions = 0;
   my $numPayorExceptions = 0;
   my $numProductTypeExceptions = 0;
   my $numRateBasisExceptions = 0;
   my $numRegionExceptions = 0;
   my $numZeroShareExceptions = 0;


   #----------------------------------
   # excount keeps track of exceptions
   #----------------------------------
   my %excount = (
      license_exists          => 0,
      track_not_found         => 0,
      album_not_found         => 0,
      zero_share              => 0,
      share_warning           => 0,
      publisher_name_mismatch => 0,
      publisher_not_found     => 0,
      bad_publisherid         => 0,
      payor_not_found         => 0,
      missing_rate_type       => 0,
      missing_product_type    => 0,
      missing_rate_basis      => 0,
      missing_lock_date       => 0,
      region_not_found        => 0,
      bad_date_format         => 0,
      invalid_digital_region  => 0,
      mechanical_exempt       => 0,
      isrc_too_long           => 0,
      isrc_too_short          => 0,
      missing_publisher_name  => 0,
   );

   #--------------------------------------
   # count keeps track of entities created
   #--------------------------------------
   my %count = (
      new_licenses => 0,
   );

   #-----------------------------
   # Loop over each row (license)
   #-----------------------------
   foreach my $row (@$rows) {

      #-----------------------------------
      # Get all of the template variables.
      #-----------------------------------
      my $rowid               = $row->{rowid};
      my $albumName           = $row->{'album-name'};
      my $catalogNumber       = $row->{'catalog-#'};
      my $trackName           = $row->{'track-name'};
      my $isrc                = $row->{'isrc'};
      my $trackMechExempt     = $row->{'track-mech-exempt'};
      my $publicDomain        = $row->{'public-domain'};
      my $productType         = $row->{'product-type'};
      my $region              = $row->{'region'};
      my $share               = $row->{'share-%'};
      my $payor               = $row->{'payor'};
      my $rsPublisherID       = $row->{'royaltyshare-publisher-id#'};
      my $publisherName       = $row->{'publisher'};
      my $publisherDirect     = $row->{'publisher-direct'};
      my $issuerLicenseID     = $row->{'issuer-license-id'};
      my $status              = $row->{'status'};
      my $crossed             = $row->{'crossed'};
      my $licenseDateSent     = $row->{'license-date-sent'};
      my $licenseDateReceived = $row->{'license-date-received'};
      my $licenseDateIssued   = $row->{'license-date-issued'};
      my $licenseTermStart    = $row->{'license-term-start'};
      my $licenseTermEnd      = $row->{'license-term-end'};
      my $rateType            = $row->{'rate-type'};
      my $ratePercentage      = $row->{'rate-%'};
      my $rateBasis           = $row->{'rate-basis'};
      my $lockDate            = $row->{'lock-date'};
      my $pennyRate           = $row->{'penny-rate'};
      my $reservePercent      = $row->{'reserve-percent'};
      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 $percentOfSales      = $row->{'percent-of-sales'};
      my $packaging           = $row->{'packaging'};
      my $freeGoods           = $row->{'free-goods'};
      my $miscDeduction       = $row->{'misc-deduction'};
      my $comments            = $row->{'comments'};
      my $balance             = $row->{'license-opening-balance'};

      my $rsTrackID           = $row->{'royaltyshare-track-id'};
      my $rsAlbumID           = $row->{'royaltyshare-album-id'};


      # Check for obsolete column names
      my $altRSTrackID        = $row->{'rs-track-id'};
      die("ERROR: found column named 'rs-track-id', expecting 'royaltyshare-track-id'") if ( $altRSTrackID );

      report("#### row($rowid) ".Dumper(\%$row));

      $publisherName =~ s/\s*$// if ( $publisherName );
      $publisherName =~ s/^\s*// if ( $publisherName );
      $albumName =~ s/\s*$// if ( $albumName );
      $albumName =~ s/^\s*// if ( $albumName );
      $isrc =~ s/\s*$// if ( $isrc );
      $isrc =~ s/^\s*// if ( $isrc );
      $catalogNumber =~ s/\s*$// if ( $catalogNumber );
      $catalogNumber =~ s/^\s*// if ( $catalogNumber );
      $productType   =~ s/\s*$// if ( $productType   );
      $productType   =~ s/^\s*// if ( $productType   );

      $rateBasis           =~ s/^\s*// if ( $rateBasis       );
      $lockDate            =~ s/^\s*// if ( $lockDate        );
      $pennyRate           =~ s/^\s*// if ( $pennyRate       );
      $ratePercentage      =~ s/^\s*// if ( $ratePercentage  );
      $licenseDateSent     =~ s/^\s*// if ( $licenseDateSent );
      $licenseDateReceived =~ s/^\s*// if ( $licenseDateReceived );
      $licenseDateIssued   =~ s/^\s*// if ( $licenseDateIssued );
      $licenseTermStart    =~ s/^\s*// if ( $licenseTermStart );
      $licenseTermEnd      =~ s/^\s*// if ( $licenseTermEnd );

      $payor      =~ s/\r\n$//g if ( $payor );
      $payor      =~ s/\x0d//g if ( $payor );

      $row->{'payor'} = $payor; # Re-write payor, just in case we had to clean it

      #------------------------------------------------
      # errorCode will hold zero or more error messages
      #------------------------------------------------
      my $errorCode;

      #--------------------------------------------------
      # errorCode will hold zero or more warning messages
      #--------------------------------------------------
      my $warningCode;


      #---------------
      # Find the track
      #---------------

      my $trackID;
      my $errFlag;

      if ( $rsTrackID ) {
         # TODO: Check the track title?
         $trackID = $rsTrackID;
      } else {

         # TODO: Add rsAlbumID support to _findTrack
         ($trackID,$errFlag) = _findTrack(
            catalog_number => $catalogNumber,
            album_name => $albumName,
            track_name => $trackName,
            isrc => $isrc,
         );
      }

      if ( $trackID ) {
         report("Found track $trackID");

         #-------------------------------------------------------------
         # If license is marked track mechanical exempt, then the track
         # will be set to not pay or accrue mechanical royalties, and
         # no licenses will be created or attached.
         #-------------------------------------------------------------
         if ( $trackMechExempt && $trackMechExempt =~ /^y/i ) {
            my $tObj = RPS::DB::Item::Track->Lookup( track_id => $trackID );
            if ( $execMode ) {
               $tObj->mechanical_exempt(1);
               $tObj->save();
	    }
            report("Debug: set track $trackID to mechanical_exempt");
            _appendString( $errorCode, "Mechanical Exempt");
            ++$excount{mechanical_exempt};
         }

      } else {

         report("_EXCEPTION: Track not found, errFlag($errFlag)");
         #push @errorList, $row;

         if ( $isrc ) {
            if ( length($isrc) > 12 ) {
               _appendString( $errorCode, "ISRC too long");
               ++$excount{isrc_too_long};
            }
            if ( length($isrc)>0 && length($isrc) < 12 ) {
               _appendString( $errorCode, "ISRC too short");
               ++$excount{isrc_too_short};
            }
         }

         if ( $errFlag == kTrackNotFound ) {
            _appendString( $errorCode, "Track not found");
            ++$excount{track_not_found};
         }

         if ( $errFlag == kAlbumNotFound ) {
            _appendString( $errorCode, "Album not found");
            ++$excount{album_not_found};
         }

         if ( $errFlag == kDuplicateTrackTitle ) {
            _appendString( $errorCode, "Duplicate track title");
            ++$excount{duplicate_track_title};
         }

      }


      #-------------------------------------------------------------------------
      # A note about error handling...
      #
      # The TrackLicense object can have errors associated with the form
      # fields; this is to allow errors to be propagated back to the user via
      # the UI.  From an importer standpoint, it would be nice to have this
      # same ability with a cell on a given row.  In the past, a single column
      # (named "error-code") would contain a brief message indicating why a 
      # given row could not be imported.
      #
      # The metadata importer does something pretty cool -- it can generate
      # a spreadsheet of the rows containing errors, and then insert an
      # Excel comment on the cell.  Thus, cells with errors can be readily
      # identified -- they'll have commments and may have cell coloring to
      # indicate error severity.  The comment itself will indicate why the cell
      # is causing an error.
      #
      # To enable such functionality in the License importer, we'll need a way
      # to treat each cell as an object with an 'error' property.  For now, this
      # is TBD and we'll use the "error-code" column.
      #-------------------------------------------------------------------------

      my $publisherID;
      my $payorID;
      my $regionID;
      my $rateTypeID;
      my $productTypeID;
      my $rateBasisID;
      my $inactiveID;
      my $crossedID;
      my $publisherDirectID;

      my $isRingtone; # set for ringtone licenses

      #--------------------------------------------------------------------------
      # We need to make sure that publicDomain is really set (i.e. is set to "Y")
      #--------------------------------------------------------------------------
      #$publicDomain = "" if ( $publicDomain =~ /n/i );
      #$publicDomain = "" if ( $publicDomain && $publicDomain !~ /y/i );
      $publicDomain = undef if ( $publicDomain && $publicDomain !~ /y/i );

      if ( !$publicDomain && 
           ( !$trackMechExempt || ($trackMechExempt && $trackMechExempt !~ m/y/i )) )
      {
         #---------------------------------------------------------
         # Check for zero share -- the TrackLicense validate method
         # isn't picking this up properly
         #---------------------------------------------------------
         if ( !$share or $share == 0 ) {
            _appendString( $errorCode, "Zero share");
            ++$excount{zero_share};
         }

         #----------------------------------
         # Make sure the share is an integer
         #----------------------------------
#         if ( $share && ! _isInteger($share) ) {
#            _appendString( $errorCode, "Share must be integer");
#            ++$excount{invalid_share};
#         }

         #-----------------------------
         # Make sure the share is range
         #-----------------------------
         if ( $share && ( $share < 0.10 || $share > 9999 ) ) {
            print "SHARE_EXCEPTION: Share must be between 0.10 and 9999\n";
            _appendString( $errorCode, "Share must be between 0.10 and 9999");
            ++$excount{invalid_share};
         }

         #------------------------------------------
         # Make sure the share is properly formatted
         #------------------------------------------
         if ( $share && ( $share !~ /^\d*[\.]?\d*$/ ) ) {
            print "SHARE_EXCEPTION: Invalid share format ($share)\n";
            _appendString( $errorCode, "Invalid share format");
            ++$excount{invalid_share_format};
         }

         #-------------------
         # find the publisher
         #-------------------
         my $actualName;
         if ( $rsPublisherID ) {

            if ( $rsPublisherID !~ /^\d+$/ || $rsPublisherID == 0 ) {
               _appendString( $errorCode, "Bad publisherID");
               ++$excount{bad_publisherid};

            } else {

               my $e;
               my $pObj = RPS::DB::Item::Publisher->Lookup( publisher_id => $rsPublisherID );

               if ( !$pObj ) {
                  report("_EXCEPTION: publisherID($rsPublisherID) is invalid!");
                  _appendString( $errorCode, "PublisherID invalid");
                  ++$excount{publisherid_invalid};
                  ++$e;
               } else {
                  $actualName = $pObj->publisher_name;
               }

               #----------------------------------------------------------
               # _if_ a publisher name was specified, make sure it matches
               #----------------------------------------------------------
               if ( $publisherName && $actualName && ( (lc $actualName) ne (lc $publisherName) ) ) {
                  report("_EXCEPTION: name_mismatch! id($rsPublisherID) "
                     . "template($publisherName) --> actual($actualName)");
                  _appendString( $errorCode, "PublisherID name mismatch");
                  ++$excount{publisher_name_mismatch};
                  ++$e;
               }

               $publisherID = $rsPublisherID if ( !$e );

            }
         } else {

            if ( !$publisherName ) {
               _appendString( $errorCode, "Missing publisher name");
               ++$excount{missing_publisher_name};
            } else {
               $publisherID = _findPublisher(
                  publisher_name => $publisherName
               );
            }
         }

         if ( (!$publisherID) or 0 == $publisherID ) {
            if ( $publisherName ) {
               report("_EXCEPTION: publisher '$publisherName' not found");
            } else {
               report("_EXCEPTION: publisher not defined");
            }
            _appendString( $errorCode, "Publisher not found");
            ++$excount{publisher_not_found};
         }

         #---------------------------------------------------------------------
         # Get the payor.  Note: from a TrackLicense perspective, if the
         # payor is blank then the default payor (a payor with is_default set)
         # will be used.  However, the license template (1.0) specifies that
         # a payor must be specified in the template and that it must be valid.
         #---------------------------------------------------------------------
         $payorID = _findPayor( payor_name => $payor ) if ( $payor );
         if ( ! $payorID ) {
            report("_EXCEPTION: payor '$payor' not found");
            _appendString( $errorCode, "Payor not found");
            ++$excount{payor_not_found};
            if ( $clientID == 149 ) { # XXX 149 = DAYWIND
               $payorID = 1; # Force payor to default
            }
         }

         #---------------
         # Get the region
         #---------------
         if ( $region && '' ne $region ) {
            $regionID = _findRegion( name => $region );
            if ( !defined $regionID ) {
               #--------------------------------------------------------------
               # NB: the UI will default to the region with
               # "is_default_mechanical_license_region".  The license template
               # however, requires a region to be defined.
               #--------------------------------------------------------------
               _appendString( $errorCode, "Region not found");
               ++$excount{region_not_found};
            }
         } else {
            _appendString( $errorCode, "Region missing");
            ++$excount{region_missing};
         }
      
         #-------------------
         # Get the rateTypeID
         #-------------------
         $rateTypeID = _getRateType( rate_type => $rateType );
         if ( !$rateTypeID ) {
            _appendString( $errorCode, "Missing rate type");
            ++$excount{missing_rate_type};
         } else {

            #-----------------------------------------------------------
            # For full (us ring) or min stat licenses, a rate percentage
            # is required.
            # NB: Full stat and US Ring Rate licenses share the same
            # rate type ID (kRateTypeFull). -ES 9/21/11
            #-----------------------------------------------------------
            if ( $rateTypeID == RPS::DB::Item::TrackLicense::kRateTypeFull or
                 $rateTypeID == RPS::DB::Item::TrackLicense::kRateTypeMinimum )
            {
              
               if ( !$ratePercentage )
               {
                  _appendString( $errorCode, "Missing rate percentage");
                  ++$excount{missing_rate_percentage};
               }
               else
               {
                  #-----------------------------------------------------------
                  # If the rate percentage is too small, the effective rate
                  # may end up (due to rounding) as zero during a mechanical
                  # run, which in turn will cause the run to die.
                  # To prevent this, we reject any rate percentages that are
                  # less than 1 (or greater than 100).  If the former is
                  # actually being used by the client -- perhaps as a means
                  # to prevent paying out on a license -- then client services
                  # should engage the client to figure out how to best
                  # accomodate their needs.
                  #-----------------------------------------------------------

                  #if ( $ratePercentage <= 1 || $ratePercentage > 100 )
                  if ( $ratePercentage <= 1 )
                  {
                     _appendString( $errorCode, "Invalid rate percentage");
                     ++$excount{invalid_rate_percentage};
                  }
               }
            }

            #----------------------------------------------------
            # For penny rate licenses, you must have a penny rate
            #----------------------------------------------------
            if ( $rateTypeID == RPS::DB::Item::TrackLicense::kRateTypePenny ) {
               if ( !$pennyRate ) {
                  _appendString( $errorCode, "Missing penny rate");
                  ++$excount{missing_penny_rate};
               } else {
                  if ( _numberTooSmall($pennyRate) ) {
                     _appendString( $errorCode, "Penny rate too small");
                     ++$excount{penny_rate_too_small};
                  }
               }
            }
         }

         #----------------------
         # Get the productTypeID
         #----------------------
         $productTypeID = _getProductTypeID( product_type => $productType );


         if ( $productTypeID && kProductTypeNotFound == $productTypeID ) {
            _appendString( $errorCode, "Missing product type");
            ++$excount{missing_product_type};
         }

         #---------------------------------------------------------------
         # Sanity check -- if the license is ALL or D, then make sure the
         # region is US or CA only
         #---------------------------------------------------------------
         if ( !$productTypeID ||
              RPS::DB::Item::Product::kProductTypeDigital == $productTypeID ) {
            if ( ! _validDigitalRegion($regionID) ) {
               _appendString( $errorCode, "Invalid digital region");
               ++$excount{invalid_digital_region};
            }
         }

         #-------------------
         # Get the rate basis
         # NOTE: Rate basis isn't valid for fixed-rate licenses.
         #-------------------
         if ( $rateTypeID && $rateTypeID != RPS::DB::Item::TrackLicense::kRateTypePenny ) {

            $rateBasisID = _getRateBasis( rate_basis => $rateBasis );
            if ( !$rateBasisID ) {
               _appendString( $errorCode, "Unknown rate basis");
               ++$excount{unknown_rate_basis};
            }

         } else {
report("DEBUG: penny rate license");
            if ( $rateBasis ) {
               # exception if the user specified a rate basis for a fixed-rate license
               _appendString( $errorCode, "Rate basis not allowed on fixed-rate license");
               ++$excount{fixed_rate_license_with_rate_basis};
            }
         }

         #------------------------------------------------------------
         # Make sure we have a lock date for lock-date based licenses,
         # and that the lock date is valid.
         #------------------------------------------------------------
         if ( $rateBasisID and RPS::DB::Item::TrackLicense::kRateBasisLock == $rateBasisID and !$lockDate ) {
            _appendString( $errorCode, "Missing Lock Date");
            ++$excount{missing_lock_date};
         }
         if ( $lockDate  ) {

            #if ( $lockDate =~ m/^(\d+)$/ ) {
            #   my $_lockDate = numericToDate( $lockDate );
            #   report("  Converted msft date $lockDate to $_lockDate");
            #   $lockDate = $_lockDate;
            #}
            $lockDate = _normalizeDate($lockDate);

            if ( ! _isValidDateFmt($lockDate)) {
               _appendString( $errorCode, "Bad lock date format");
               ++$excount{bad_date_format};
            } else {
               $row->{'lock-date'} = $lockDate;
            }
         }
#report("DEBUG: line $rowid, rateTypeID($rateTypeID) pennyRate($pennyRate) ratePercentage($ratePercentage) rateBasisID($rateBasisID)");
         # Can't have a lock date with a fixed-rate license (2/28/11)
#         if( $rateBasisID && $rateBasisID == RPS::DB::Item::TrackLicense::kRateBasisLock &&
#             $rateTypeID && $rateTypeID == RPS::DB::Item::TrackLicense::kRateTypePenny ) {
#            _appendString( $errorCode, "Lock date not allowed on fixed-rate license");
#            ++$excount{lock_date_penny_rate_license};
#         }

         #-------------------------
         # Get the publisher direct
         #-------------------------
         $publisherDirectID = _getPublisherDirect( publisher_direct => $publisherDirect );

         #------------------------
         # Get the inactive status
         #------------------------
         $inactiveID = _getStatusID( inactive => $status );

         #-----------------
         # Get the cross ID
         #-----------------
         $crossedID = _getCrossedID( crossed => $crossed );


         #------------------------------------#
         #                                    #
         #  Special validation for ringtones  #
         #                                    #
         #------------------------------------#

         if ( $productTypeID &&
              RPS::DB::Item::Product::kProductTypeRingtone == $productTypeID ) {

            $isRingtone = 1;

            #-----------------------------------------
            # Make sure rate type is 'R' or 'P' or 'S'/'F'
            #
            # NB: We check the raw value vs the rateTypeID because US Ring Rate
            # and Full Stat share the same rateTypeID, however only the former
            # is valid with ringtone products. -ES 9/20/11
            #
            # Relaxed to include 's' 'f' (full statutory), in addition to fixed ('p')
            # and US ringtone ('r'). -ES 7/20/15
            #
            #-----------------------------------------
            if ( $rateType !~ /^(p|r|s|f)/i ) {
               _appendString( $errorCode, "Invalid ringtone rate type");
               ++$excount{invalid_ringtone_rate_type};
            }

            #-------------------------------------
            # Ringtones are valid only in US or CA
            #-------------------------------------
            report("WARNING: Missing regionID for region '$region' (errorCode = '$errorCode')") if ( !defined $regionID );
            if ( defined $regionID && ! _validDigitalRegion($regionID) ) {
               _appendString( $errorCode, "Invalid ringtone region");
               ++$excount{invalid_ringtone_region};
            }

            #-----------------------------------
            # Ringtone licenses can't be crossed
            #-----------------------------------
            if ( $crossedID ) {
               _appendString( $errorCode, "Can't cross ringtone license");
               ++$excount{cant_cross_ringtone};
            }

            #----------------------------------
            # Reserves not allowed on ringtones
            #----------------------------------
            if ( $reservePercent || $p1 || $p2 || $p3 || $p4 ||
                 $p5 || $p6 || $p7 || $p8 ) {
               _appendString( $errorCode, "No reserves on ringtones");
               ++$excount{no_reserves_on_ringtones};
            }

            #---------------------------------
            # Ringtones can't be public domain
            #---------------------------------
            if ( $publicDomain )
            {
               _appendString( $errorCode, "Ringtone can't be public domain");
               ++$excount{no_pd_on_ringtones};
            }

            #-------------------------------
            # Ringtones can't hava a balance
            #-------------------------------
            if ( $balance )
            {
               _appendString( $errorCode, "Ringtone license balance not supported");
               ++$excount{no_balance_on_ringtones};
            }

         }#ringtone validation
         else
         {
            if ( $rateType =~ /^r$/i ) {
               _appendString( $errorCode, "Can't use ringtone rate type on non-ringtone license");
               ++$excount{ringtone_rate_on_non_ring_license};
            }
         }



         #--------------------------------------------------
         # Make sure that any dates are in the proper format
         #--------------------------------------------------
         if ( $licenseDateSent  ) {
            $licenseDateSent = _normalizeDate($licenseDateSent);
            if ( ! _isValidDateFmt($licenseDateSent)) {
               _appendString( $errorCode, "Bad date sent format");
               ++$excount{bad_date_format};
            } else {
               $row->{'license-date-sent'} = $licenseDateSent;
            }
         }

         if ( $licenseDateReceived  ) {
            $licenseDateReceived = _normalizeDate($licenseDateReceived);
            if ( ! _isValidDateFmt($licenseDateReceived)) {
               _appendString( $errorCode, "Bad date received format");
               ++$excount{bad_date_format};
            } else {
               $row->{'license-date-received'} = $licenseDateReceived;
            }
         }

         if ( $licenseDateIssued  ) {
            $licenseDateIssued = _normalizeDate($licenseDateIssued);
            if ( ! _isValidDateFmt($licenseDateIssued)) {
               _appendString( $errorCode, "Bad date issued format");
               ++$excount{bad_date_format};
            } else {
               $row->{'license-date-issued'} = $licenseDateIssued;
            }
         }

         if ( $licenseTermStart  ) {
            $licenseTermStart = _normalizeDate($licenseTermStart);
            if ( ! _isValidDateFmt($licenseTermStart)) {
               _appendString( $errorCode, "Bad term start format");
               ++$excount{bad_date_format};
            } else {
               $row->{'license-term-start'} = $licenseTermStart;
            }
            #else { report(" termStart '$licenseTermStart' is valid"); }
         }

         if ( $licenseTermEnd  ) {
            $licenseTermEnd = _normalizeDate($licenseTermEnd);
            if ( ! _isValidDateFmt($licenseTermEnd)) {
               _appendString( $errorCode, "Bad term end format");
               ++$excount{bad_date_format};
            } else {
               $row->{'license-term-end'} = $licenseTermEnd;
            }
         }

         #----------------------------------
         # If a reserve percentage is specified then a liquidation
         # schedule is required.
         #----------------------------------
         if ( $reservePercent && !$p1 && !$p2 && !$p3 && !$p4 &&
              !$p5 && !$p6 && !$p7 && !$p8 ) {

            # For ringtone licenses, the liquidation schedule is N/A so
            # suppress the error.  FB14736 11/4/11
            #
            if ( !$isRingtone )
            {
               _appendString( $errorCode, "Liquidation Schedule cannot be blank");
               ++$excount{blank_liquidation_schedule};
            }
         }

         #-------------------------------------------------------
         # Make sure the sum of the liquidation schedule is valid
         #-------------------------------------------------------
         my $_scheduleTotal = 0;
         $_scheduleTotal += $p1 if ( $p1 );
         $_scheduleTotal += $p2 if ( $p2 );
         $_scheduleTotal += $p3 if ( $p3 );
         $_scheduleTotal += $p4 if ( $p4 );
         $_scheduleTotal += $p5 if ( $p5 );
         $_scheduleTotal += $p6 if ( $p6 );
         $_scheduleTotal += $p7 if ( $p7 );
         $_scheduleTotal += $p8 if ( $p8 );
         if ( $_scheduleTotal > 100 )
         {
            _appendString( $errorCode, "Total amount liquidated cannot exceed 100");
            ++$excount{liquidation_schedule_exceeded};
         }


      }# non-PD specific fields

      #-------------------------------------------------------
      # For PD licenses, the share can't be zero otherwise the
      # form validator will complain.
      #-------------------------------------------------------
      if ( $publicDomain ) {
         if ( !$share || $share == 0 ) {
            _appendString( $errorCode, "Zero PD share");
            ++$excount{zero_pd_share};
            #die("zero PD share");
         }
      }

      #------------------------------------------------------------
      # If we have any errors at this point then set the errorCode.
      #------------------------------------------------------------
      $row->{'error-code'} = $errorCode;

      #-------------------------------------------------------------------
      # Are there any exceptions that the form validation won't pickup?
      # If so, stop processing this license.
      #
      # * If we don't have a trackID then we're done (the form validator
      #   requires a trackID)
      # * If this is a regular publishing license and we don't have a
      #   publisherID then we're done.
      # * If this is a regular publishing license and we don't have a
      #   share then we're done.
      # * If this is a regular publishing license and we don't have a
      #   valid product type then we're done.
      #-------------------------------------------------------------------

      if ( !$trackID || (!$publisherID && (!$publicDomain)) ||
           (!$publicDomain && (!$share or $share == 0) )  ||
           (!$publicDomain && ($share && ( $share < 0.01 || $share > 9999 ) )) ||
           (!$publicDomain && !$regionID )  ||
           (!$publicDomain && !$payorID )  ||
           (!$publicDomain && !$rateTypeID )  ||
           (!$publicDomain && $rateTypeID && RPS::DB::Item::TrackLicense::kRateTypePenny == $rateTypeID && !$pennyRate ) ||
           (!$publicDomain && $productTypeID && ($productTypeID == kProductTypeNotFound ) )  )
      {
         next;
      };

      if ( $publicDomain && (!$share || $share == 0) )
      {
         report("WARNING: PD license with zero share.. skipping: $errorCode");
         next;
      }

      if ( !$publicDomain && $reservePercent && !$p1 && !$p2 && !$p3 && !$p4 &&
           !$p5 && !$p6 && !$p7 && !$p8 )
      {
         report("WARNING: Skipping line $rowid with missing liquidation schedule");
         next;
      }

      if ( $errorCode )
      {
         report("WARNING: uncaught error condition exists! errorCode: $errorCode");
         next;
      }

      #===========================================
      # Ok, go ahead and create the license object
      #===========================================

      #----------------------------------------------------------
      # Check if the license already exists.
      #----------------------------------------------------------
      my %tlArgs = (
         track_id => $trackID
      );

      #
      # The region from the template may refer to multiple regions
      # as defined by region_country_map; thus when checking for
      # existing licenses we need to check for licenses on countries
      # defined by region_country_map. -ES 8/13/10
      #

      if ( $publicDomain )
      {
         $tlArgs{type} = RPS::DB::Item::TrackLicense::kPublicDomain;
      }
      else
      {

         if ( $isRingtone )
         {
            $tlArgs{type} = RPS::DB::Item::TrackLicense::kRingtone;
         }
         else
         {
            $tlArgs{type} = RPS::DB::Item::TrackLicense::kPublishingLicense;
         }
         $tlArgs{publisher_id} = $publisherID;
         $tlArgs{region_id} = $regionID;
         $tlArgs{product_type_id} = $productTypeID if ( $productTypeID );
         #$tlArgs{payor_id} = $payorID;
      }


      # Removing this validation check... -ES 1/15/13
      #
#      my $tlObj = RPS::DB::Item::TrackLicense->Lookup( %tlArgs );
#      if ( $tlObj )
#      {
#         my $licID = $tlObj->track_license_id;
#
#         report("   WARNING: license $licID exists: ". Dumper(\%tlArgs));
#
#         if ( $publicDomain )
#         {
#            _appendString( $errorCode, "PD License Exists");
#            ++$excount{pd_license_exists};
#         }
#         else
#         {
#            _appendString( $errorCode, "License Exists");
#            ++$excount{license_exists};
#         }
#         $row->{'error-code'} = $errorCode;
#         next;
#      }


      #--------------------------------------------------------------
      # Create a RPS::License::TrackLicense object so we can validate
      # the license information using the form validation logic
      #--------------------------------------------------------------
      my %licArgs = (
         _loadSubs => 1,
         trackID => $trackID,
      );
      if ( $publicDomain )
      {
         report("PD detected");
         $licArgs{publicDomain} = 1;

         report("PD debug: " . Dumper(\%licArgs));
      }
      #...........................................................
      # Note: the object will default to kPublishingLicense if the
      # publicDomain flag is omitted during instantiation
      #...........................................................

      my $newLicense;


      my $financeAccount; # set only if there's a balance

      my $validPendingTransaction = 0;     # true if PendingTransaction is valid
      my $validPendingTransactionList = 0; # true if PendingTransactionList is valid
      my $validFinanceAccount = 0;         # true if Account is valid
      my $validReserveLiquidation = 0;     # true if ReserveLiquidation is valid

      if ( ! $publicDomain )
      {

         if ( $isRingtone )
         {
            $newLicense = RPS::License::US::RingtoneLicense->new( %licArgs );
         }
         else
         {
            $newLicense = RPS::License::US::TrackLicenseWithLiquidationSchedule->new( %licArgs );
         }

         $newLicense->Inactive($inactiveID);

         $newLicense->PublisherID($publisherID) if $publisherID;
         $newLicense->Share($share) if $share;
         $newLicense->PayorID($payorID) if $payorID;
         $newLicense->RegionID($regionID) if $regionID;
         $newLicense->RateType($rateTypeID) if $rateTypeID;

         $newLicense->PennyRate($pennyRate)
            if (  RPS::DB::Item::TrackLicense::kRateTypePenny == $rateTypeID );

         $newLicense->RatePercentage($ratePercentage)
            if (  RPS::DB::Item::TrackLicense::kRateTypePenny != $rateTypeID && $ratePercentage);

         $newLicense->ProductTypeID($productTypeID);
         $newLicense->RateBasis($rateBasisID) if ( $rateBasisID );

         $newLicense->ReservePercentage($reservePercent) if ( $reservePercent );

         if ( $rateBasisID && $rateBasisID == RPS::DB::Item::TrackLicense::kRateBasisLock ) {
            $newLicense->LockDate($lockDate);
         }

         $newLicense->IssuerLicenseID($issuerLicenseID);
         $newLicense->Comments($comments);
         $newLicense->MiscDeduction($miscDeduction);
         $newLicense->FreeGoods($freeGoods);

         $newLicense->DateSent($licenseDateSent) if ($licenseDateSent );

         $newLicense->DateReceived($licenseDateReceived) if ($licenseDateReceived );

         $newLicense->DateIssued($licenseDateIssued) if ($licenseDateIssued );

         $newLicense->TermStart($licenseTermStart) if ($licenseTermStart );

         $newLicense->TermEnd($licenseTermEnd) if ($licenseTermEnd );

         $newLicense->CrossCollateralized($crossedID);
         $newLicense->PublisherDirect($publisherDirectID);

         $newLicense->PercentageOfSales($percentOfSales) if ( $percentOfSales );

         #-------------------------
         # Set the reserve schedule
         #-------------------------
         if ( !$isRingtone )
         {
            my $reserveLiquidationList = $newLicense->{ReserveLiquidationList};
            my $reserveArray = $reserveLiquidationList->getList();

            _setReserve(1,$p1,\%{$reserveArray->[0]} );
            _setReserve(2,$p2,\%{$reserveArray->[1]} );
            _setReserve(3,$p3,\%{$reserveArray->[2]} );
            _setReserve(4,$p4,\%{$reserveArray->[3]} );
            _setReserve(5,$p5,\%{$reserveArray->[4]} );
            _setReserve(6,$p6,\%{$reserveArray->[5]} );
            _setReserve(7,$p7,\%{$reserveArray->[6]} );
            _setReserve(8,$p8,\%{$reserveArray->[7]} );
            $validReserveLiquidation = $reserveLiquidationList->validate();
         }

         #-----------------------------------------------------------
         # If there's a balance, we need to setup the finance account
         #-----------------------------------------------------------
         if ( $balance )
         {
            $financeAccount = $newLicense->{FinanceAccount};
            $financeAccount->TypeCode(
               RPS::DB::Item::FinanceAccount::kAccountTypeHoldover
            );
            $financeAccount->CurrencyCode('USD');

            my $ptList = $financeAccount->{PendingTransactionList};

# TODO: hack... if PendingTransactionList isn't defined, let's create it here...
#
            if( not defined $ptList ) # XXX XXX XXX
            {
                report("PendingTransactionList _not_ defined ... creating it");
                $financeAccount->{PendingTransactionList} = RPS::Finance::PendingTransactionList->new(
                    accountID => $financeAccount->AccountID(),
                    startDate => $financeAccount->{_startDate},
                    endDate => $financeAccount->{_endDate},
                    showPendingPaymentsOnly => $financeAccount->{_showPendingPaymentsOnly},
                );
                $ptList = $financeAccount->{PendingTransactionList};
            }
die("PendingTransactionList _not_ defined".  Dumper( \%$financeAccount )) if( not defined $ptList ); # XXX XXX should not happen
#            my $ptArray = $ptList->getPendingTransactionArray();

# XXX In Case 3442, there was a balance (albeit malformed) that caused this
# XXX section of code to execute.  For some reason ptList came back empty
# XXX so we got an error when trying to invoke getPendingTransactionArray.
# XXX
# XXX  >>> The PendingTransactionList _should_ have a PendingTransactionArray
# XXX      tied to it.
report("DEBUG: financeAccount: ". Dumper( \%$financeAccount ) ); # XXX
#
# XXX ptArray is supposed to be defined within the finance account object (RPS::Finance::Account)
#   but for some bizarre reason it's not being set when the finance account is created.
#   So, we create the pending transaction but it never makes it onto the pending transaction list
#   (ptArray) because well, we're not getting the reference $financeAccount->{PendingTransactionList}
#   What we're trying to do is create a pending transaction and add it to the
#   finance account's pending transaction list...
#
#   If I'm understanding RPS::Finance::Account correctly, PendingTransactionList should be an
#   empty array for a new Account object.
#   UPDATE: loadSubs is 0, so Account isn't creating the PendingTransactionList (see Account->_initSubs).
#   UPDATE: TrackLicenseWithLiquidationSchedule.pm creates Account object w/ loadSubs => 0
#           This causes a fatal problem when we try to save the new license object:
#           When saving the license, the Account object itself is saved.  In Account->save, once an
#           finance_account_id is defined, it tries to apply the account id to all of the underlying
#           pending transactions:
#
#              if (! $accountID)
#              {
#                  $accountID = $dbObj->finance_account_id();
#                  $self->{PendingTransactionList}->assignAccountID($accountID, $dbObj->currency_code);
#              }
#
#              $self->{PendingTransactionList}->save();
#
#           !! assignAccountID dies because PendingTransactionList was never created in the first place.
#
#            my $ptArray = (defined $ptList) ? $ptList->getPendingTransactionArray() : [ ] ; # XXX - debug
            my $ptArray = $ptList->getPendingTransactionArray(); # XXX - TEST
            report("ptArray is empty") if ( not defined $ptArray->[0] );

            #..................................................................
            # Note: passing the args as shown below has no effect. The 'amount'
            # and 'type_code' must be set after instantiation.
            #
            # Meanwhile, the following "new" call will generate a warning in
            # RPS::Finance::PendingTransaction.pm due to type_code not being
            # set.  We manually set values immediately after instantiation so
            # it should all be good.
            #..................................................................
            my $ptObj = RPS::Finance::PendingTransaction->new(
               amount => $balance,
               type_code => RPS::DB::Item::PendingTransaction::kTypeAdjustment,
            );

            $ptObj->Amount($balance);
            $ptObj->TypeCode( RPS::DB::Item::PendingTransaction::kTypeAdjustment );
            $ptObj->CurrencyCode('USD');
            $ptObj->Memo("Opening Balance");
            push @{$ptArray}, $ptObj;

            report("ptArray is still empty") if ( not defined $ptArray->[0] ); # XXX - TEST

            #-----------------------------
            # Validate the finance objects
            #-----------------------------

            # TODO: if an object won't validate, we need to find the error
            # and then add it into the errorCode variable
            $validFinanceAccount = $financeAccount->validate();

# XXX 2/9/15 - PendingTransactionList not working, so the validate() stuff
# XXX isn't working here.  I think it's because the FinanceAccount hasn't
# XXX been created yet.  Let's try deferring the balance creation until
# XXX the license has been created.

#if( $clientID == 16 )
#{
#            $validPendingTransactionList = 1; # XXX XXX XXX
#            $validPendingTransaction = 1; # XXX XXX XXX
#}
            $validPendingTransactionList = $ptList->validate();
            $validPendingTransaction = $ptObj->validate();


            report("DEBUG: Checking if ptArray is defined...");
            if ( not defined $ptArray->[0] )
            {
               report("ptArray is empty");
            }
            else
            {
               my $num = @$ptArray;
               report("ptArray is not empty, has $num element(s) ");
               foreach my $p (@{$ptArray})
               {
                  die("   amount not defined? ". ref $p )
                     if ( not defined $p->Amount() ); # XXX
                  my $amt = $p->Amount();

                  report("   $amt");
               }
            }
         }
         else
         {
            #-----------------------------------------------------------
            # This license doesn't have a balance, but we need to set
            # the following validation flags so that we don't mistakenly
            # think the license isn't valid
            #-----------------------------------------------------------
            $validFinanceAccount = 1;
            $validPendingTransactionList = 1;
            $validPendingTransaction = 1;
         }

      }
      else
      {
         #...............................................................
         # This doesn't work -- the publicDomain flag _must_ be set when
         # creating the TrackLicense object in order for it to be flagged
         # as PD.  If we leave out the publicDomain flag at creation and
         # then try to use the "Type" modifier to set the license as PD,
         # the TrackLicense validator tries to use the non-PD validation
         # logic to examine the license and of course it fails since the
         # PD license is missing a bunch of required fields.
         #...............................................................
         # $newLicense->Type(RPS::DB::Item::TrackLicense::kPublicDomain);

         $newLicense = RPS::License::US::PublicDomain->new( %licArgs );

         # Set the share.  It must be defined and > 0.
         assert($share);
         $newLicense->Share($share);

         #----------------------------------------------------------------
         # Even though this is a PD license, we need to set the following
         # validation flags so that we don't mistakenly flag the license
         # as invalid
         #----------------------------------------------------------------
         $validFinanceAccount = 1;
         $validPendingTransactionList = 1;
         $validPendingTransaction = 1;
      }

      #--------------------------------------------------------------------
      # In order for the license to validate properly, any sub-objects must
      # also validate successfully.
      #--------------------------------------------------------------------
      
      report("DEBUG: ReserveLiquidation validation:  $validReserveLiquidation");

      report("DEBUG: finance validation:  account($validFinanceAccount) "
         . "ptList($validPendingTransactionList) pt($validPendingTransaction)");

      my $validLicense = $newLicense->validate();

      #-----------------------------------------------------------------------
      # If the license didn't validate, see if we can get a meaningful message
      # from the form element.
      # If the _only_ error is a share exception, then we'll let the license in
      # _if_ the share is over 100%.  Per CD, this is to accomodate licenses
      # where the client intentionally wants to license over a 100%.
      #-----------------------------------------------------------------------
      my %errorMap;
      _getError(\%$newLicense, \%errorMap);
      if ( (keys %errorMap) == 1 )
      {
         report("Share error: ".  $newLicense->Share() ) if (exists $errorMap{Share});
         if (exists $errorMap{Share} && $newLicense->Share() > 100 ) {
            $validLicense = 1; # overlook this error...
         }
      }


      if ( $validLicense && $validFinanceAccount && $validPendingTransactionList &&
           $validPendingTransaction )
      {
         report("#### VALID ####");

         if ( $execMode )
         {

            #----------------------------------------------------------
            # Everything is valid, so go ahead and save off the objects
            #----------------------------------------------------------
            my $_faID;
            if ( $financeAccount )
            {
               $financeAccount->save(); # saves finance_account & pending_transaction
               my $financeAccountID = $financeAccount->AccountID();
               $_faID = $financeAccountID;
               $newLicense->FinanceAccountID($financeAccountID);


               # XXX 2/9/15  If there's a balance, create the pending transaction and tag it onto
               # the finance account
#               if( $balance )
#               {
#                  my $ptObj = RPS::Finance::PendingTransaction->new(
#                     amount => $balance,
#                     type_code => RPS::DB::Item::PendingTransaction::kTypeAdjustment,
#                  );
#
#                  $ptObj->Amount($balance);
#                  $ptObj->TypeCode( RPS::DB::Item::PendingTransaction::kTypeAdjustment );
#                  $ptObj->CurrencyCode('USD');
#                  $ptObj->Memo("Opening Balance");
#
#                  $ptObj->FinanceAccountID( $financeAccountID );
#                  $ptObj->save();
#                  $_ptID = $ptObj->pending_transaction_id;
#               }
            }
            
            $newLicense->save(); # saves track_license and reserve_liquidation
            ++$count{new_licenses};
            ++$numNewLicenses;

            my $trackLicenseID = $newLicense->TrackLicenseID();

            if( $balance && $_faID )
            {
                report("  Created track_license $trackLicenseID w/ financeAccountID $_faID (balance $balance)");
            }
            else
            {
                report("  Created track_license $trackLicenseID");
            }

            #-------------------------------------------------------------
            # If we let this license in with a share exception, flag it so
            # it's visible in the import report.
            #-------------------------------------------------------------
            if ( exists $errorMap{Share} )
            {
               _appendString( $warningCode, "Share warning");
               ++$excount{share_warning};
               $row->{'warning-code'} = $warningCode;
            }

            #----------------------------------------------------------------
            # The finance account (if any) and track license both know about
            # each other. 
            #
            # After creating the license with the financeAccountID info, we
            # need to update the financeAccount object with the license info.
            #----------------------------------------------------------------
            if ( $financeAccount ) {
               $financeAccount->Description("advance account for license $trackLicenseID");
               $financeAccount->save();
            }

            $row->{'rs-license-id'} = $trackLicenseID;
            report("   rowid($rowid): Created license $trackLicenseID");
         }
      }
      else
      {
         #-------------------------------------------
         # This whole section needs to be revamped...
         #-------------------------------------------
         #report("INVALID");
         report("INVALID, regionID($regionID) pubID($publisherID) ptype($productTypeID): ". Dumper(\%errorMap) );

         report("    validLicense($validLicense) validFinanceAccount($validFinanceAccount) "
            . "validPendingTransactionList($validPendingTransactionList) "
            . "validPendingTransaction($validPendingTransaction )");

         if ( $errorMap{RegionID} )
         {
#            report("INVALID, region error with license!");
#            _appendString( $errorCode, "Region error");
#            ++$excount{region_error};
#            $row->{'error-code'} = $errorCode;

            # If we get here, validate the license using the region logic
            # from RPS/License/BaseTrackLicense.pm.  This will allow us
            # to properly get the reason why the region isn't valid.
            # Note that we replace $self with $newLicense, and we skip
            # the form-related data.

# XXX Start copy from RPS/License/BaseTrackLicense.pm XXX

#            my $countryCodeMap = $self->_getCountryCodeMapForValidation();
            my $countryCodeMap = $newLicense->_getCountryCodeMapForValidation();

            # Make sure _our_ region doesn't have any of these countries in it.
            #
#            my $ccMapItems = RPS::DB::Item::RegionCountryMap->GetByRegionID($self->RegionID());
            my $ccMapItems = RPS::DB::Item::RegionCountryMap->GetByRegionID($newLicense->RegionID());
            while (my $mapItem = $ccMapItems->next())
            {
                if ($countryCodeMap->{$mapItem->country_code})
                {
                    print "DEBUG: TrackLicense::validate - this track, publisher, and product type is already licensed on country code " . $mapItem->country_code . "\n";
#                    $self->{RegionID}->setError(Common::FormObject::kErrFieldInvalid);
#                    $valid = 0;
                    _appendString( $errorCode, "Track, publisher, and product type already licensed on country code " . $mapItem->country_code);
                    ++$excount{region_error};
                    $row->{'error-code'} = $errorCode;
                    last;
                }
            }

# XXX End copy from RPS/License/BaseTrackLicense.pm XXX



         }
         else
         {
            # If you get here, figure out what went wrong
            die("ERROR: RPS license validation failed but license importer didn't recognize it");
         }

      }

   }# row loop

   #------------------------------------------------------------
   # Dump out the errors
   # TODO: Need to properly propagate the errors back to the user
   #------------------------------------------------------------
   _showExceptions( $rows );

   #-----------------------
   # Show import statistics
   #-----------------------
   report("##### S U M M A R Y #####");
   my $totalExceptions = 0;
   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 );
   report(" ");
   
   foreach my $c (keys %count) {
      my $v = $count{$c};
      printf("%30s %d\n", $c, $v);
   }
   report("sanity check: total licenses created = $numNewLicenses");
}#_processData

#--------------------------------------------------------------------------
# _showExceptions was originally intended to _just_ show the template lines
# that exceptioned out.  It's been modified to output both exception and
# non-exception lines.  Exception message(s) will be placed in the error
# code column.  If a line is imported successfully, then the resulting
# licenseID will be stored in the import status column ("licenseID(###)"),
# otherwise this column will contain the string "__FAILED__".
#--------------------------------------------------------------------------
sub _showExceptions {
   my ( $rows ) = @_;

   #---------------------------
   # Build the exception header
   #---------------------------
   my @header;
   for( my $i = 0; $i < (keys %gColumnMap); $i++ ) {
      # Get the key at the specified column
      my $v = $gColumnMap{$i};
      next if ( !$v ); # strip blank columns

      #----------------------------------------------------------
      # Do _not_ push the "Error Code" or "import-status" columns
      # if they were in the original template.  We'll re-create
      # them on the fly.
      #----------------------------------------------------------
      next if ( ("Error Code" eq $v) || ("import-status") eq $v );

      push @header, $v;
   }

   report("EXCEPTION:\t".join("\t", @header, "Error Code", "import-status"));

   #--------------------------------------
   # Now dump out the rows that had errors
   #--------------------------------------
   foreach my $row (@$rows) {
      my $rowid = $row->{rowid};
      my $errorCode = $row->{'error-code'};
      my $warningCode = $row->{'warning-code'};
      my $licenseID = $row->{'rs-license-id'} || '---';

      #----------------------------------------
      # Output the row data in the proper order
      #----------------------------------------
      my @obuf;
      for( my $i = 0; $i < (keys %gColumnMap); $i++ ) {
         # Get the key at the specified column
         my $v = $gColumnMap{$i};
         next if ( !$v ); # strip blank columns
         my $val = ($row->{$v}) ? $row->{$v} : '';

         #----------------------------------------------------------
         # Do _not_ push the "Error Code" or "import-status" columns
         # if they were in the original template.  We'll re-create
         # these columns below.
         #----------------------------------------------------------
         next if ( ("Error Code" eq $v) || ("import-status") eq $v );

         push @obuf, $val;
      }

      my $importStatus;
      my $ecString;

      if ( $errorCode ) {
         $importStatus = "__FAIL__";
         $ecString = ($errorCode) ? $errorCode : '';
      } else {
         $importStatus = "license($licenseID)";
         $ecString = ($warningCode) ? $warningCode : '';
      }

      my $zzbuf = join("\t", "EXCEPTION:", @obuf, $ecString, $importStatus);

      report($zzbuf);
   }
}

#-------------------------------------------------------------
# _setReserve -- defines a ReserveLiquidation item if there is
# any percentage to liquidate
#-------------------------------------------------------------
sub _setReserve {
   my($period,$percent,$a) = @_;
   $percent = 0 if ( !$percent );
   $a->Period($period);
   $a->Percent($percent);
}

#-------------------------------------------------------------
# _dumpError -- go through all of the TrackLicense properties
# and display the current value (if any), underlying datatype,
# and error information
#-------------------------------------------------------------
sub _dumpError {
   my($leader, $license) = @_;

   #-------------------------------------------------------
   # The following will show _all_ properties (not just the
   # usual track_license info)
   #-------------------------------------------------------
   foreach my $k (sort { $a cmp $b } keys %$license ) {

      my $v = $license->{$k};
      my $typeOf = ref $v;

      my $hasError = '';
      if ( (ref $v) =~ /Common::FormObject::Scalar/ ||
           ref $v eq "Common::FormObject::DateTime" ) {

         my $sv = (defined $v->_getValue()) ? $v->_getValue() : '__undef__';
         $hasError = $v->_hasError();
         my $errFlag = $hasError ? "*** ERROR ***" : '';
         printf("%s   %20s  %20s %s %s\n", $leader, $k, $sv, $typeOf, $errFlag);

      } elsif( (ref $v) =~ /RPS::Finance::PendingTransactionList/ ) {

         my $pArray = $v->getPendingTransactionArray();
         my $pType = ref $pArray;
         $hasError = $v->_hasError();
         my $errFlag = $hasError ? "*** ERROR ***" : '>> OK <<';

         printf("%s %s %s\n", $leader, $typeOf, $errFlag );
         foreach my $pt (@{$pArray}) {
            my $amt = $pt->Amount();
            my $typeCode = $pt->TypeCode();
            $pType = ref $pt;
            $hasError = $pt->_hasError();
            my $errFlag = $hasError ? "*** ERROR ***" : ' >> OK <<';
            report("$leader    $pType: amount($amt) typeCode($typeCode) $errFlag");
         }



      } elsif( (ref $v) =~ /RPS::Finance::Account/ ) {
         $hasError = $v->_hasError();
         my $errFlag = $hasError ? "*** ERROR ***" : '>> OK <<';

         printf("%s   %20s  %20s %s %s\n", $leader, $k, " ", $typeOf, $errFlag);
         my $financeAccountID = $v->AccountID();

         _dumpError( $leader . "      ",\%$v );

      } elsif( (ref $v) =~ /RPS::License::ReserveLiquidationList/ ) {

         my $reserveArray = $v->getList();
            
         printf("%s   %20s  %s\n%20s", $leader, $k, "@", $typeOf);
         for( my $i=0; $i<8; $i++ ) {
            printf("%s [%d] %d,", $leader, $i, $reserveArray->[$i]->Percent() );
         }
         print"\n";
      } else {
         my $errFlag = '';

         $v = "---" if ( !$v );
         printf("%s   %20s  %20s %s %s\n", $leader, $k, $v, $typeOf, $errFlag);
      }
   }
}# _dumpError

#---------------------------------------------------
# _isValidDateFmt - return true if the supplied date
# is in the format YYYY-MM-DD, false otherwise
#---------------------------------------------------
sub _isValidDateFmt {
   my($dstr) = @_;

   my($yr,$mo,$dy) = split("-",$dstr);

   # Normally we're expecting YYYY-MM-DD

   my $st = 1; # valid unless we detect otherwise

   $st = 0 if ( !$yr || !$mo || !$dy );
   $st = 0 if ( $mo && ( $mo !~ /^\d+$/ ));
   $st = 0 if ( $yr && ( $yr !~ /^\d+$/ ));
   $st = 0 if ( $dy && ( $dy !~ /^\d+$/ ));
   $st = 0 if ( length($yr) != 4 );

   $st = 1 if( $dstr =~ /(\d{1,2})-(\d{1,2})-(\d{2})/ ); # MM-DD-YY

   if( $dstr =~ /(\d{4})-(\d{1,2})-(\d{1,2})/ ) # YYYY-MM-DD
   {
      return ( $3 > Days_in_Month($1, $2) ) ? 0 : 1; 
   }

   return $st;
}

sub _normalizeDate {
   my($dstr) = @_;

   if( $dstr =~ /^\d{5}$/ ) {
      my $_dt = numericToDate( $dstr );
      report("_normalizeDate:  Converted msft date $dstr to $_dt");
      $dstr = $_dt;
   }
   elsif( $dstr =~ /^(\d{1,2})-(\d{1,2})-(\d{2})$/ ) { # MM-DD-YY
      $dstr = sprintf("%04d-%02d-%02d", $3 + 2000, $1, $2);
   }
   return $dstr;
}

sub _isInteger {
   my($v) = @_;
   return ( $v and $v =~ m/^\d*$/ ) ? 1 : undef;
}

sub _findTrack {
   my(%args) = @_;
   my $trackID;
   my ($albumID,$errflag) = _findAlbum(%args);
   if ( ! $albumID ) {
      my $albumName = $args{album_name};
      my $catNo = $args{catalog_number} || "";
      report("   _findTrack: album($albumName) cat#($catNo) not found, skipping track");
      $errflag = kAlbumNotFound;
   } else {
      my $trackName = $args{track_name};
      $trackName =~ s/^\s+//; # remove leading spaces
      $trackName =~ s/\s+$//; # remove trailing spaces

      my $isrc = (defined $args{isrc}) ? $args{isrc} : "";
      report("   _findTrack: looking for track($trackName) isrc($isrc) on albumID($albumID)");


      my $duplicate; # set if track title is not unique
      if ( !$isrc ) {
         # No ISRC; see if track title is a duplicate
         my $cleanTitle = clean_name_catalog($trackName);
         my $sql = "SELECT track_id FROM track "
            . "WHERE album_id = $albumID AND "
            #. "( title = ". $dbo->DBQuote($trackName) . " OR title_clean LIKE '$cleanTitle%' ) ";
            . "( title = ". $dbo->DBQuote($trackName) . " OR title_clean = '$cleanTitle' ) ";
         my $sth = $dbo->DoCmd($sql);
         if ( $sth->rows > 1 ) {
            report("_findTrack:DEBUG: duplicate title detected: sql = $sql");
            $errflag = kDuplicateTrackTitle;
            $duplicate = 1;
         }
      }

      if ( ! $duplicate ) {
         my $tColl = RPS::DB::Item::Track->GetTracksByAlbumID($albumID);
         while( $tColl->hasNext() ) {
            my $trackObj = $tColl->next();

            if ( $isrc ) {
               $isrc = trimspaces($isrc);
               my $masterID = $trackObj->master_id;
               my $mObj = RPS::DB::Item::Master->Lookup( master_id => $masterID );
               my $zzIsrc = $mObj->isrc;
               #if ( '' ne $isrc && $isrc eq $zzIsrc )

               my $t1 = lc clean_name_catalog($trackName);
               my $t2 = lc $trackObj->title_clean;

               my $actualTitle = $trackObj->title;

               # kludge for New World
               #$t2 =~ s/__/_/g if ( $clientID == 138 );

               $t2 =~ s/^_//g if ( $clientID == 290 ); # fearless

               # kludge for Nettwerk; remove trailing "_" from clean title
               $t2 =~ s/_$// if ( $clientID == 51 );

               # kludge for EGR; remove trailing "_" from clean title
               $t2 =~ s/_$// if ( $clientID == 183 );

               # kludge for VP Records; remove trailing "_" from clean title (FB13163)
               #$t2 =~ s/_$// if ( $clientID == 306 );
               if ( $clientID == 306 )
               {
                  $t1 =~ s/_$//;
                  $t2 =~ s/_$//;
               }

#               report("_findTrack:2: comparing titles [$t1] to [$t2], isrc [$isrc] to [$zzIsrc]");

               if( $clientID == 16 )  # XXX  SFW kludge
               {
                  report("_findTrack:SFW: comparing isrc [$isrc] to [$zzIsrc] - ignoring titles [$t1] to [$t2]");

                  # 2/11/15 - Now we're back to using ISRC and ignoring title ... oh eff me
                  # ignore track name, use ISRC for matching
#                  if( $isrc && $zzIsrc && '' ne $isrc && ($isrc eq $zzIsrc) )
#                  {
#                     $trackID = $trackObj->track_id;
#                     last;
#                  }
#                  # 2/10/15 - Now SFW wants to ignore ISRC and use the title ...
#                  # 2/12/15 - Now SFW wants to ignore ISRC and use the title ...
#
                  if( ($t1 eq $t2) || ( lc $actualTitle eq lc $trackName ))
                  {
                     $trackID = $trackObj->track_id;
                     last;
                  }
               }

               else
               {  # normal matching behavior for everyone else besides SFW

                  report("_findTrack:2: comparing titles [$t1] to [$t2], isrc [$isrc] to [$zzIsrc]");

                  # The ISRC and title must match
                  # If the RS track title matches but doesn't have an ISRC, then allow
                  # the match.
                  #
                  if ( $isrc && $zzIsrc && '' ne $isrc && ($isrc eq $zzIsrc) &&
                      (($t1 eq $t2) || ( lc $actualTitle eq lc $trackName )) )
                  {
                     $trackID = $trackObj->track_id;
                     last;
                  }
               }# XXX

            } else {
               my $t1 = lc clean_name_catalog($trackName);
               my $t2 = lc $trackObj->title_clean;

               # kludge for New World
               $t2 =~ s/__/_/g if ( $clientID == 138 );

               $t2 =~ s/^_//g if ( $clientID == 290 ); # fearless

               # kludge for Nettwerk; remove trailing "_" from clean title
               $t2 =~ s/_$//;

               # kludge for VP Records; remove trailing "_" from clean title (FB13163)
               $t2 =~ s/_$// if ( $clientID == 306 );

               report("_findTrack: comparing [$t1] to [$t2]");

               if ( $t1 eq $t2 ) {
                  $trackID = $trackObj->track_id;
                  last;
               }
            }
         }#track loop

         $errflag = kTrackNotFound if( !$trackID );
      }


   }
   return ($trackID, $errflag);
}# _findTrack

sub _findAlbum {
   my(%args) = @_;
   my $catNo = $args{catalog_number} || "";
   my $albumName = $args{album_name};
   my $errflag;
   my $albumID;
   if ( $albumName && $clientID != 16 ) {
      my %a = (
         title => $albumName,
      );
      $a{catalog_number} = $catNo if ( $catNo );
      #report("   _findAlbum: looking for CAT#($catNo) title($albumName)");

      my $cleanAlbumTitle = clean_name_catalog($albumName);
      report("   _findAlbum: looking for CAT#($catNo) title($albumName) titleClean($cleanAlbumTitle)");

      #-------------------------------------------------------------------
      # Warning: if a title has a trailing space in the database, then the
      # clean version will have a trailing underscore.  We use LIKE to try
      # and prevent this from not finding the album.
      #-------------------------------------------------------------------

      #my $sql = "SELECT album.* FROM album WHERE title_clean LIKE '$cleanAlbumTitle%' ";

      #my $sql = "SELECT album.* FROM album WHERE "
      #   . "( title = ". $dbo->DBQuote($albumName) . " OR title_clean LIKE '$cleanAlbumTitle%' ) ";

      my $sql = "SELECT album.* FROM album WHERE "
         . "( title = ". $dbo->DBQuote($albumName) . " OR title_clean = '$cleanAlbumTitle' ) ";
      $sql .= " AND catalog_number = " . $dbo->DBQuote($catNo) if ( $catNo );

report("D: _findAlbum: sql = $sql"); # XXX XXX
      my $albumColl = RPS::DB::Item::Album->GetAll( $sql );

      $errflag = kAlbumNotFound if ( $albumColl->size() == 0 );
      $errflag = kNonUniqueAlbumName if ( $albumColl->size() > 1 );

      if ( !$errflag ) {
         my $aObj = $albumColl->next();
         $albumID = $aObj->album_id;
         report("   _findAlbum: found albumID($albumID)");
      }
   }
   elsif( $clientID == 16 )   # XXX XXX SFW kludge - ignore album name, search using catno
   {
      assert($catNo);
      my $sql = "SELECT album.* FROM album WHERE "
         . "catalog_number = " . $dbo->DBQuote($catNo);

      my $albumColl = RPS::DB::Item::Album->GetAll( $sql );

      $errflag = kAlbumNotFound if ( $albumColl->size() == 0 );
      $errflag = kNonUniqueAlbumName if ( $albumColl->size() > 1 );

      if ( !$errflag ) {
         my $aObj = $albumColl->next();
         $albumID = $aObj->album_id;
         report("   _findAlbum_SFW: found albumID($albumID)");
      }
   }
   return ($albumID, $errflag);
}# _findAlbum

sub _findPublisher {
   my(%args) = @_;
   my $publisherID;
   my $name = $args{publisher_name};
   $name =~ s/\s*$//g;
   $name =~ s/^\s*//g;
   my $pObj = RPS::DB::Item::Publisher->Lookup(
      publisher_name => $name,
   );
   # TODO: Should check for duplicate publisher names..
   if ( $pObj ) {
      $publisherID = $pObj->publisher_id;
   }
   return $publisherID;
}

sub _findPayor {
   my(%args) = @_;
   my $payorID;
   my $pObj = RPS::DB::Item::Payor->Lookup(
      name => $args{payor_name},
   );
   if ( $pObj ) {
      $payorID = $pObj->payor_id;
   }
   return $payorID;
}

sub _findRegion {
   my(%args) = @_;
   my $regionID;
   my $regionName = $args{name};

   my $rObj = RPS::DB::Item::Region->Lookup(
      name => $regionName,
   );
   if ( $rObj ) {
      $regionID = $rObj->region_id;
   }
   return $regionID;
}

#--------------------------------------------------------
# For 'ALL' or 'D' licenses, the region must be US or CA.
# This routine checks the specified region and sees if it
# contains any other countries besides US and/or CA
#--------------------------------------------------------
sub _validDigitalRegion {
   my($regionID) = @_;
   my $valid = 1;
   my $coll = RPS::DB::Item::RegionCountryMap->GetByRegionID($regionID);
   while($valid && $coll->hasNext() ) {
      my $rcObj = $coll->next();
      my $cc = uc $rcObj->country_code;
      $valid = 0 if ( $cc ne 'US' && $cc ne 'CA' );
   }
   return $valid;

}
#-----------------------------------------------------------
# _getCrossedID -- returns a value that can be used to set a
# license's cross_collateralized field
#-----------------------------------------------------------
sub _getCrossedID {
   my(%args) = @_;
   my $crossed = $args{crossed};
   my $crossedID = 0; # no - default
   if ( $crossed && $crossed =~ /^y/i ) {
      $crossedID = 1; 
   }
   return $crossedID;
}

#----------------------------------------------------------
# _getStatusID -- returns a value that can be used to set a
# license's 'inactive' field
#----------------------------------------------------------
sub _getStatusID {
   my(%args) = @_;
   my $status = $args{inactive};
   my $statusID = 0; # active - default
   if ( $status && $status =~ /^i/i ) {
      $statusID = 1; 
   }
   return $statusID;
}

#----------------------------------------------------------
# _getPublisherDirect -- returns a value that can be used
# to set a license's 'publisher_direct' field.
#----------------------------------------------------------
sub _getPublisherDirect {
   my(%args) = @_;
   my $pdflag = $args{publisher_direct};
   my $statusID = 0; # default is "not publisher direct"
   if ( $pdflag && $pdflag =~ /^y/i ) {
      $statusID = 1;  # publisher direct
   }
   return $statusID;
}

#--------------------------------------------------------------
# _getRateBasis -- returns a rate basis identifier based on the
# supplied text string
#--------------------------------------------------------------
sub _getRateBasis {
   my(%args) = @_;
   my $rateBasis = $args{rate_basis};
   my $rateBasisID;
   if ( !$rateBasis || ($rateBasis && $rateBasis =~ /^S/i ) ) {
      $rateBasisID = RPS::DB::Item::TrackLicense::kRateBasisSale; # Default
   } elsif ( $rateBasis && $rateBasis =~ /^L/i ) {
      $rateBasisID = RPS::DB::Item::TrackLicense::kRateBasisLock;
   }
   return $rateBasisID;
}

#-----------------------------------------------------------------
# _getRateType -- returns an RPS rate type identifier based on the
# supplied rate type string.
# If a rate type isn't found, then an undefined rate type ID will
# be returned to the caller.
#-----------------------------------------------------------------
sub _getRateType {
   my(%args) =@_;
   my $rateType = $args{rate_type};
   my $rateTypeID = undef;

   # Note: the 1.0 spec defines S, M and P as valid rate types
   # I'm including F(ULL), M(IN), P(ENNY) for now.
   if ( $rateType && '' ne $rateType ) {
      if ( $rateType =~ /^p/i ) {

         $rateTypeID = RPS::DB::Item::TrackLicense::kRateTypePenny;

      } elsif( ($rateType =~ /^s/i) || ($rateType =~ /^f/i) ||
               ($rateType =~ /ring/i) || ($rateType =~ /r/i ) ) {

         $rateTypeID = RPS::DB::Item::TrackLicense::kRateTypeFull;

      } elsif( $rateType =~ /^m/i ) {

         $rateTypeID = RPS::DB::Item::TrackLicense::kRateTypeMinimum;
      }
   } else {
      $rateTypeID = RPS::DB::Item::TrackLicense::kRateTypeFull; # Default
   }

   return $rateTypeID;
}

sub _getProductTypeID {
   my(%args) = @_;
   my $cfg = $args{product_type};
   #return RPS::DB::Item::ProductType::kAllProducts if ( $cfg =~ /^all$/i );
   return kProductTypeNotFound if ( !$cfg );
   return undef if ( $cfg =~ /^all$/i || $cfg =~ /^all products$/i );

   return 254 if ( ($cfg =~ /^alld$/i) || ($cfg =~ /^all-d$/i) ||
                 ($cfg =~ /^all digital$/i) || ($cfg =~ /^all-digital$/i) ); # All digital

   return RPS::DB::Item::ProductType::kAllPhysicalProducts if ( ($cfg =~ /^allp$/i) ||
      ($cfg =~ /^all-p$/i) || ($cfg =~ /^all physical$/i ) );

   return RPS::DB::Item::Product::kProductTypeLP if ( $cfg =~ /lp/i );
   return RPS::DB::Item::Product::kProductTypeCD if ( $cfg =~ /cd/i );
   return RPS::DB::Item::Product::kProductTypeDigital if ( $cfg =~ /^da$/i );
   return RPS::DB::Item::Product::kProductTypeDigitalTrack if ( $cfg =~ /^dt$/i );
   return RPS::DB::Item::Product::kProductTypeVHS if ( $cfg =~ /^vhs$/i );
   return RPS::DB::Item::Product::kProductTypeCass if ( $cfg =~ /^cass$/i );
   return RPS::DB::Item::Product::kProductTypeEP if ( $cfg =~ /^ep$/i );
   return RPS::DB::Item::Product::kProductTypeDVD if ( $cfg =~ /^dvd$/i );
   return RPS::DB::Item::Product::kProductTypeCDSingle if ( $cfg =~ /^cd5$/i );
   return RPS::DB::Item::Product::kProductTypeCassSingle if ( $cfg =~ /^cas5$/i );
   return RPS::DB::Item::Product::kProductTypeDVDCDSet if ( $cfg =~ /^dvdcd$/i );
   return RPS::DB::Item::Product::kProductTypeDblCD if ( $cfg =~ /^cd2$/i );
   return RPS::DB::Item::Product::kProductTypeRingtone if ( $cfg =~ /^ring$/i || $cfg =~ /^rt$/i );
   return RPS::DB::Item::Product::kProductTypeLP5 if ( $cfg =~ /^lp5$/i );
   return kProductTypeNotFound;
}

sub _appendString {
   my($str,$v) = @_;

   if ( $str ) {
      my $cur = $str;
      my $newstring = "$cur; $v";
      $_[0] = $newstring;
   } else {
      $_[0] = $v;
   }
   return;
die("str($str) v($v)") if ( !$str );

   my $cur = $str;
   my $prefix = "$cur; $v";
   $_[0] = ($str) ? $prefix : $v;
}# _appendString

sub _getError {
   my($licensehash, $errormap) = @_;
   #-------------------------------------------------------
   # The supplied hash is assumed to be a hash of form
   # elements.  Check each one for an error...
   #-------------------------------------------------------
   foreach my $k (sort { $a cmp $b } keys %$licensehash ) {

      my $v = $licensehash->{$k};
      my $typeOf = ref $v;

      my $hasError = '';

      if ( (ref $v) =~ /Common::FormObject::Scalar/ ||
           (ref $v) eq "Common::FormObject::DateTime" ) {

         if ( $v->_hasError() ) {
            my %xmlParams = $v->getXMLParams();
            my $msg = defined $xmlParams{emsg} ? $xmlParams{emsg} : "MSG_NOT_AVAILABLE";
            my $e = $xmlParams{e};
            $errormap->{$k} = $msg;
         }
      }
   }

}# _getError2

sub _reportError {
   my ($name, $obj) = @_;

   report("   _reportError: checking '$name' for errors");
   if ($obj && $obj->_hasError()) {

      my %xmlParams = $obj->getXMLParams();
      my $msg = defined $xmlParams{emsg} ? $xmlParams{emsg} : "MSG_NOT_AVAILABLE";
      my $e = $xmlParams{e};
      print "$name has an error:: $e :: $msg\n";
      return 1;
   }
   return undef;
}

sub report {
   my($text, $level) = @_;
   $level = kNormal unless $level;
   if ( $level <= $gReportLevel ) {
      print $text . "\n";
   }
}

sub _numberTooSmall {
   my($n) = @_;
   #return if ( $n );
   return ($n && ($n < 0.0001)) ? 1 : 0;
}

sub numericToDate
{
   #my $self = shift;
   my( $ndays ) = @_;

   ## Excel date is (supposedly) days since 01 Jan 1900, but have to subtract 2
   ## from that number because (1) 0/1 index issue, and (2) MS wants to pretend
   ## that 1900 was a leap year.
   my @date = Add_Delta_Days(1900, 1, 1, $ndays - 2);
   return wantarray ? @date : sprintf("%d-%02d-%02d", @date);
}

1;
