+ # cch specific
+
+ my ( $imported, $last, $min_sec ) = _progressbar_foo();
+ my $count = scalar(keys %$taxproduct);
+ foreach my $pkgpart ( keys %$taxproduct ) {
+ warn "restoring taxproductnums on pkgpart $pkgpart\n" if $DEBUG;
+ if ( time - $min_sec > $last ) {
+ $job->update_statustext(
+ int( 100 * $imported / $count ). ",Restoring tax products"
+ );
+ $last = time;
+ }
+ $imported++;
+
+ my $part_pkg = qsearchs('part_pkg', { pkgpart => $pkgpart } );
+ unless ( $part_pkg ) {
+ return "somehow failed to find part_pkg with pkgpart $pkgpart!\n";
+ }
+
+ my %options = $part_pkg->options;
+ my %pkg_svc = map { $_->svcpart => $_->quantity } $part_pkg->pkg_svc;
+ my $primary_svc = $part_pkg->svcpart;
+ my $new = new FS::part_pkg { $part_pkg->hash };
+
+ foreach my $class ( keys %{ $taxproduct->{$pkgpart} } ) {
+ warn "working with class '$class'\n" if $DEBUG;
+ my $part_pkg_taxproduct =
+ qsearchs( 'part_pkg_taxproduct',
+ { taxproduct => $taxproduct->{$pkgpart}->{$class},
+ data_vendor => $format,
+ }
+ );
+
+ unless ( $part_pkg_taxproduct ) {
+ return "failed to find part_pkg_taxproduct (".
+ $taxproduct->{$pkgpart}->{$class}. ") for pkgpart $pkgpart\n";
+ }
+
+ if ( $class eq '' ) {
+ $new->taxproductnum($part_pkg_taxproduct->taxproductnum);
+ next;
+ }
+
+ $options{"usage_taxproductnum_$class"} =
+ $part_pkg_taxproduct->taxproductnum;
+
+ }
+
+ my $error = $new->replace( $part_pkg,
+ 'pkg_svc' => \%pkg_svc,
+ 'primary_svc' => $primary_svc,
+ 'options' => \%options,
+ );
+
+ return $error if $error;
+
+ }
+
+ '';
+}
+
+sub _restore_remembered_disabled_taxes {
+ my ( $job, $format, $disabled_tax_rate ) = @_;
+
+ my ( $imported, $last, $min_sec ) = _progressbar_foo();
+ my $count = scalar(keys %$disabled_tax_rate);
+ foreach my $key (keys %$disabled_tax_rate) {
+ if ( time - $min_sec > $last ) {
+ $job->update_statustext(
+ int( 100 * $imported / $count ). ",Disabling tax rates"
+ );
+ $last = time;
+ }
+ $imported++;
+ my ($geocode,$taxclass) = split /:/, $key, 2;
+ my @tax_class = qsearch( 'tax_class', { data_vendor => $format,
+ taxclass => $taxclass,
+ } );
+ return "found multiple tax_class records for format $format class $taxclass"
+ if scalar(@tax_class) > 1;
+
+ unless (scalar(@tax_class)) {
+ warn "no tax_class for format $format class $taxclass\n";
+ next;
+ }
+
+ my @tax_rate =
+ qsearch('tax_rate', { data_vendor => $format,
+ geocode => $geocode,
+ taxclassnum => $tax_class[0]->taxclassnum,
+ }
+ );
+
+ if (scalar(@tax_rate) > 1) {
+ return "found multiple tax_rate records for format $format geocode ".
+ "$geocode and taxclass $taxclass ( taxclassnum ".
+ $tax_class[0]->taxclassnum. " )";
+ }
+
+ if (scalar(@tax_rate)) {
+ $tax_rate[0]->disabled('Y');
+ my $error = $tax_rate[0]->replace;
+ return $error if $error;
+ }
+ }
+}
+
+sub _remove_old_tax_data {
+ my ( $job, $format ) = @_;
+
+ my $dbh = dbh;
+ my $error = $job->update_statustext( "0,Removing old tax data" );
+ die $error if $error;
+
+ my $sql = "UPDATE public.tax_rate_location SET disabled='Y' ".
+ "WHERE data_vendor = ". $dbh->quote($format);
+ $dbh->do($sql) or return "Failed to execute $sql: ". $dbh->errstr;
+
+ my @table = qw(
+ tax_rate part_pkg_taxrate part_pkg_taxproduct tax_class cust_tax_location
+ );
+ foreach my $table ( @table ) {
+ $sql = "DELETE FROM public.$table WHERE data_vendor = ".
+ $dbh->quote($format);
+ $dbh->do($sql) or return "Failed to execute $sql: ". $dbh->errstr;
+ }
+
+ if ( $format eq 'cch' ) {
+ $sql = "DELETE FROM public.cust_tax_location WHERE data_vendor = ".
+ $dbh->quote("$format-zip");
+ $dbh->do($sql) or return "Failed to execute $sql: ". $dbh->errstr;
+ }
+
+ '';
+}
+
+sub _create_temporary_tables {
+ my ( $job, $format ) = @_;
+
+ my $dbh = dbh;
+ my $error = $job->update_statustext( "0,Creating temporary tables" );
+ die $error if $error;
+
+ my @table = qw( tax_rate
+ tax_rate_location
+ part_pkg_taxrate
+ part_pkg_taxproduct
+ tax_class
+ cust_tax_location
+ );
+ foreach my $table ( @table ) {
+ my $sql =
+ "CREATE TEMPORARY TABLE $table ( LIKE $table INCLUDING DEFAULTS )";
+ $dbh->do($sql) or return "Failed to execute $sql: ". $dbh->errstr;
+ }
+
+ '';
+}
+
+sub _copy_from_temp {
+ my ( $job, $format ) = @_;
+
+ my $dbh = dbh;
+ my $error = $job->update_statustext( "0,Making permanent" );
+ die $error if $error;
+
+ my @table = qw( tax_rate
+ tax_rate_location
+ part_pkg_taxrate
+ part_pkg_taxproduct
+ tax_class
+ cust_tax_location
+ );
+ foreach my $table ( @table ) {
+ my $sql =
+ "INSERT INTO public.$table SELECT * from $table";
+ $dbh->do($sql) or return "Failed to execute $sql: ". $dbh->errstr;
+ }
+
+ '';
+}
+
+=item process_download_and_reload
+
+Download and process a tax update as a queued JSRPC job after wiping the
+existing wipeable tax data.
+
+=cut
+
+sub process_download_and_reload {
+ _process_reload(\&process_download_and_update, @_);
+}
+
+#
+#
+# END OF CCH STUFF
+#
+#
+
+=item process_batch_reload
+
+Load and process a tax update from the provided files as a queued JSRPC job
+after wiping the existing wipable tax data.
+
+=cut
+
+sub process_batch_reload {
+ _process_reload(\&_perform_batch_import, @_);
+}
+
+sub _process_reload {
+ my ( $continuation, $job, $param ) = @_;
+ my $format = $param->{'format'};
+
+ my ( $imported, $last, $min_sec ) = _progressbar_foo();
+
+ if ( $job ) { # progress bar
+ my $error = $job->update_statustext( 0 );
+ die $error if $error;
+ }
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+ my $error = '';
+
+ if ( $format =~ /^cch/ ) {
+ # no, THIS part is CCH specific
+
+ my $sql =
+ "SELECT count(*) FROM part_pkg_taxoverride JOIN tax_class ".
+ "USING (taxclassnum) WHERE data_vendor = '$format'";
+ my $sth = $dbh->prepare($sql) or die $dbh->errstr;
+ $sth->execute
+ or die "Unexpected error executing statement $sql: ". $sth->errstr;
+ die "Don't (yet) know how to handle part_pkg_taxoverride records."
+ if $sth->fetchrow_arrayref->[0];
+
+ # really should get a table EXCLUSIVE lock here
+
+ #remember disabled taxes
+ my %disabled_tax_rate = ();
+ $error ||= _remember_disabled_taxes( $job, $format, \%disabled_tax_rate );
+
+ #remember tax products
+ my %taxproduct = ();
+ $error ||= _remember_tax_products( $job, $format, \%taxproduct );
+
+ #create temp tables
+ $error ||= _create_temporary_tables( $job, $format );
+
+ #import new data
+ unless ($error) {
+ eval { &{$continuation}( $job, $param ) };
+ $error = $@ if $@;
+ }
+
+ #restore taxproducts
+ $error ||= _restore_remembered_tax_products( $job, $format, \%taxproduct );
+
+ #disable tax_rates
+ $error ||=
+ _restore_remembered_disabled_taxes( $job, $format, \%disabled_tax_rate );
+
+ #wipe out the old data
+ $error ||= _remove_old_tax_data( $job, $format );
+
+ #untemporize
+ $error ||= _copy_from_temp( $job, $format );
+
+ } elsif ( $format =~ /^billsoft-(\w+)/ ) {
+
+ my $mode = $1;
+ my @sql;
+ if ( $mode eq 'pcode' ) {
+ push @sql,
+ "DELETE FROM cust_tax_location WHERE data_vendor = 'billsoft'",
+ "UPDATE tax_rate_location SET disabled = 'Y' WHERE data_vendor = 'billsoft'";
+ } elsif ( $mode eq 'taxclass' ) {
+ push @sql,
+ "DELETE FROM tax_class WHERE data_vendor = 'billsoft'";
+ } elsif ( $mode eq 'taxproduct' ) {
+ push @sql,
+ "DELETE FROM part_pkg_taxproduct WHERE data_vendor = 'billsoft'";
+ }
+
+ foreach (@sql) {
+ if (!$dbh->do($_)) {
+ $error = $dbh->errstr;
+ last;
+ }
+ }
+
+ unless ( $error ) {
+ local $@;
+ eval { &{ $continuation }($job, $param) };
+ $error = $@;
+ }
+ } # if ($format ...)
+
+ if ($error) {
+ $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
+ die $error;
+ }
+
+ #success!
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+}
+
+
+=item process_download_and_update
+
+Download and process a tax update as a queued JSRPC job
+
+=cut
+
+sub process_download_and_update {
+ my $job = shift;
+
+ my $param = shift;
+ my $format = $param->{'format'}; #well... this is all cch specific
+
+ my ( $imported, $last, $min_sec ) = _progressbar_foo();
+
+ if ( $job ) { # progress bar
+ my $error = $job->update_statustext( 0);
+ die $error if $error;
+ }
+
+ my $cache_dir = '%%%FREESIDE_CACHE%%%/cache.'. $FS::UID::datasrc. '/';
+ my $dir = $cache_dir. 'taxdata';
+ unless (-d $dir) {
+ mkdir $dir or die "can't create $dir: $!\n";
+ }
+
+ if ($format eq 'cch') {
+
+ my @namelist = qw( code detail geocode plus4 txmatrix zip );
+
+ my $conf = new FS::Conf;
+ die "direct download of tax data not enabled\n"
+ unless $conf->exists('taxdatadirectdownload');
+ my ( $urls, $username, $secret, $states ) =
+ $conf->config('taxdatadirectdownload');
+ die "No tax download URL provided. ".
+ "Did you set the taxdatadirectdownload configuration value?\n"
+ unless $urls;
+
+ $dir .= '/cch';