+#sub get_district_methods {
+# '' => '',
+# 'wa_sales' => 'Washington sales tax',
+#};
+
+=item get_district LOCATION METHOD
+
+For the location hash in LOCATION, using lookup method METHOD, fetch
+tax district information. Currently the only available method is
+'wa_sales' (the Washington Department of Revenue sales tax lookup).
+
+Returns a hash reference containing the following fields:
+
+- district
+- tax (percentage)
+- taxname
+- exempt_amount (currently zero)
+- city, county, state, country (from
+
+The intent is that you can assign this to an L<FS::cust_main_county>
+object and insert it if there's not yet a tax rate defined for that
+district.
+
+get_district will die on error.
+
+=over 4
+
+=cut
+
+sub get_district {
+ no strict 'refs';
+ my $location = shift;
+ my $method = shift or return '';
+ warn Dumper($location, $method) if $DEBUG;
+ &$method($location);
+}
+
+
+=head2 wa_sales location_hash
+
+Expects output of location_hash() as parameter
+
+Returns undef on error, or if tax rate cannot be found using given address
+
+Query the WA State Dept of Revenue API with an address, and return
+tax district information for that address.
+
+Documentation for the API can be found here:
+
+L<https://dor.wa.gov/find-taxes-rates/retail-sales-tax/destination-based-sales-tax-and-streamlined-sales-tax/wa-sales-tax-rate-lookup-url-interface>
+
+This API does not return consistent usable county names, as the county
+name may include appreviations or labels referring to PTBA (public transport
+benefit area) or CEZ (community empowerment zone). It's recommended to use
+the tool freeside-wa-tax-table-update to fully populate the
+city/county/districts for WA state every financial quarter.
+
+Returns a hashref with the following keys:
+
+ - district the wa state tax district id
+ - tax the combined total tax rate, as a percentage
+ - city the API rate name
+ - county The API address PTBA
+ - state WA
+ - country US
+ - exempt_amount 0
+
+If api returns no district for address, generates system log error
+and returns undef
+
+=cut
+
+sub wa_sales {
+
+ #
+ # no die():
+ # freeside-queued will issue dbh->rollback on die() ... this will
+ # also roll back system log messages about errors :/ freeside-queued
+ # doesn't propgate die messages into the system log.
+ #
+
+ my $location_hash = shift;
+
+ # Return when called with pointless context
+ return
+ unless $location_hash
+ && ref $location_hash
+ && $location_hash->{state} eq 'WA'
+ && $location_hash->{address1}
+ && $location_hash->{zip}
+ && $location_hash->{city};
+
+ my $log = FS::Log->new('wa_sales');
+
+ warn "wa_sales() called with location_hash:\n".Dumper( $location_hash)."\n"
+ if $DEBUG;
+
+ my $api_url = 'http://webgis.dor.wa.gov/webapi/AddressRates.aspx';
+ my @api_response_codes = (
+ 'The address was found',
+ 'The address was not found, but the ZIP+4 was located.',
+ 'The address was updated and found, the user should validate the address record',
+ 'The address was updated and Zip+4 located, the user should validate the address record',
+ 'The address was corrected and found, the user should validate the address record',
+ 'Neither the address or ZIP+4 was found, but the 5-digit ZIP was located.',
+ 'The address, ZIP+4, and ZIP could not be found.',
+ 'Invalid Latitude/Longitude',
+ 'Internal error'
+ );
+
+ my %get_query = (
+ output => 'xml',
+ addr => $location_hash->{address1},
+ city => $location_hash->{city},
+ zip => substr( $location_hash->{zip}, 0, 5 ),
+ );
+ my $get_string = join '&' => (
+ map{ sprintf "%s=%s", $_, uri_escape( $get_query{$_} ) }
+ keys %get_query
+ );
+
+ my $prepared_url = "${api_url}?$get_string";
+
+ warn "API call to URL: $prepared_url\n"
+ if $DEBUG;
+
+ my $dom;
+ local $@;
+ eval { $dom = XML::LibXML->load_xml( location => $prepared_url ); };
+ if ( $@ ) {
+ my $error =
+ sprintf "Problem parsing XML from API URL(%s): %s",
+ $prepared_url, $@;
+
+ $log->error( $error );
+ warn $error;
+ return;
+ }
+
+ my ($res_root) = $dom->findnodes('/response');
+ my ($res_addressline) = $dom->findnodes('/response/addressline');
+ my ($res_rate) = $dom->findnodes('/response/rate');
+
+ my $res_code = $res_root->getAttribute('code')
+ if $res_root;
+
+ unless (
+ ref $res_root
+ && ref $res_addressline
+ && ref $res_rate
+ && $res_code <= 5
+ && $res_root->getAttribute('rate') > 0
+ ) {
+ my $error =
+ sprintf
+ "Problem querying WA DOR tax district - " .
+ "code( %s %s ) " .
+ "address( %s ) " .
+ "url( %s )",
+ $res_code || 'n/a',
+ $res_code ? $api_response_codes[$res_code] : 'n/a',
+ $location_hash->{address1},
+ $prepared_url;
+
+ $log->error( $error );
+ warn "$error\n";
+ return;
+ }
+
+ my %response = (
+ exempt_amount => 0,
+ state => 'WA',
+ country => 'US',
+ district => $res_root->getAttribute('loccode'),
+ tax => $res_root->getAttribute('rate') * 100,
+ county => uc $res_addressline->getAttribute('ptba'),
+ city => uc $res_rate->getAttribute('name')
+ );
+
+ $response{county} =~ s/ PTBA//i;
+
+ if ( $DEBUG ) {
+ warn "XML document: $dom\n";
+ warn "API parsed response: ".Dumper( \%response )."\n";
+ }
+
+ my $info_message =
+ sprintf
+ "Tax district(%s) selected for address(%s %s %s %s)",
+ $response{district},
+ $location_hash->{address1},
+ $location_hash->{city},
+ $location_hash->{state},
+ $location_hash->{zip};
+
+ $log->info( $info_message );
+ warn "$info_message\n"
+ if $DEBUG;
+
+ \%response;
+
+}
+
+###### USPS Standardization ######
+
+sub standardize_usps {
+ my $class = shift;
+
+ eval "use Business::US::USPS::WebTools::AddressStandardization";
+ die $@ if $@;
+
+ my $location = shift;
+ if ( $location->{country} ne 'US' ) {
+ # soft failure
+ warn "standardize_usps not for use in country ".$location->{country}."\n";
+ $location->{addr_clean} = '';
+ return $location;
+ }
+ my $userid = $conf->config('usps_webtools-userid');
+ my $password = $conf->config('usps_webtools-password');
+ my $verifier = Business::US::USPS::WebTools::AddressStandardization->new( {
+ UserID => $userid,
+ Password => $password,
+ Testing => 0,
+ } ) or die "error starting USPS WebTools\n";
+
+ my($zip5, $zip4) = split('-',$location->{'zip'});
+
+ my %usps_args = (
+ FirmName => $location->{company},
+ Address2 => $location->{address1},
+ Address1 => $location->{address2},
+ City => $location->{city},
+ State => $location->{state},
+ Zip5 => $zip5,
+ Zip4 => $zip4,
+ );
+ warn join('', map "$_: $usps_args{$_}\n", keys %usps_args )
+ if $DEBUG > 1;
+
+ my $hash = $verifier->verify_address( %usps_args );
+
+ warn $verifier->response
+ if $DEBUG > 1;
+
+ die "USPS WebTools error: ".$verifier->{error}{description} ."\n"
+ if $verifier->is_error;
+
+ my $zip = $hash->{Zip5};
+ $zip .= '-' . $hash->{Zip4} if $hash->{Zip4} =~ /\d/;