Avalara Tax API Integration in Java: Complete Guide

Avalara provides automated tax calculation, compliance, and reporting solutions. This guide covers complete integration with Avalara's AvaTax API in Java/Spring Boot.


1. Project Setup and Dependencies

Maven Dependencies (pom.xml)

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>avalara-tax-integration</artifactId> <version>1.0.0</version> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <spring.boot.version>2.7.14</spring.boot.version> <jackson.version>2.15.2</jackson.version> <okhttp.version>4.11.0</okhttp.version> </properties> <dependencies> <!-- Spring Boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>${spring.boot.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> <version>${spring.boot.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> <version>${spring.boot.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> <version>${spring.boot.version}</version> </dependency> <!-- HTTP Client --> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>${okhttp.version}</version> </dependency> <!-- JSON Processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>${jackson.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> <version>${jackson.version}</version> </dependency> <!-- Database --> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <!-- Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- Cache --> <dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> <version>3.1.6</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${spring.boot.version}</version> </plugin> </plugins> </build> </project>

2. Configuration Classes

Application Properties (application.yml)

# Avalara Configuration avalara: base-url: ${AVALARA_BASE_URL:https://sandbox-rest.avatax.com} account-id: ${AVALARA_ACCOUNT_ID:} license-key: ${AVALARA_LICENSE_KEY:} company-code: ${AVALARA_COMPANY_CODE:DEFAULT} timeout: 30000 max-retries: 3 machine-name: ${HOSTNAME:tax-service} app-name: TaxService app-version: 1.0 environment: ${AVALARA_ENVIRONMENT:sandbox} # Cache Configuration cache: tax-rates: ttl: 3600 # 1 hour # Server Configuration server: port: 8080 # Database spring: datasource: url: jdbc:h2:mem:testdb driverClassName: org.h2.Driver username: sa password: jpa: database-platform: org.hibernate.dialect.H2Dialect hibernate: ddl-auto: create-drop show-sql: true h2: console: enabled: true path: /h2-console # Logging logging: level: com.example.avalara: DEBUG

Avalara Configuration Class

import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Data @Component @ConfigurationProperties(prefix = "avalara") public class AvalaraConfig { private String baseUrl; private String accountId; private String licenseKey; private String companyCode = "DEFAULT"; private int timeout = 30000; private int maxRetries = 3; private String machineName = "tax-service"; private String appName = "TaxService"; private String appVersion = "1.0"; private String environment = "sandbox"; }

3. Database Entities

Tax Transaction Entity

import lombok.Data; import javax.persistence.*; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.Map; @Entity @Table(name = "tax_transactions") @Data public class TaxTransaction { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "transaction_code", unique = true) private String transactionCode; @Column(name = "avalara_transaction_id") private String avalaraTransactionId; @Column(name = "document_type") @Enumerated(EnumType.STRING) private DocumentType documentType; @Column(name = "transaction_date") private LocalDateTime transactionDate; @Column(name = "customer_code") private String customerCode; @Column(name = "company_code") private String companyCode; @Column(name = "total_amount", precision = 15, scale = 4) private BigDecimal totalAmount; @Column(name = "total_tax", precision = 15, scale = 4) private BigDecimal totalTax; @Column(name = "taxable_amount", precision = 15, scale = 4) private BigDecimal taxableAmount; @Column(name = "exempt_amount", precision = 15, scale = 4) private BigDecimal exemptAmount; @Column(name = "currency_code") private String currencyCode = "USD"; @Column(name = "origin_address_id") private Long originAddressId; @Column(name = "destination_address_id") private Long destinationAddressId; @Column(name = "status") @Enumerated(EnumType.STRING) private TransactionStatus status; @Column(name = "commit_status") private Boolean commitStatus = false; @Column(name = "tax_calculation_date") private LocalDateTime taxCalculationDate; @Column(name = "created_at") private LocalDateTime createdAt; @Column(name = "updated_at") private LocalDateTime updatedAt; @PrePersist protected void onCreate() { createdAt = LocalDateTime.now(); updatedAt = LocalDateTime.now(); if (transactionDate == null) { transactionDate = LocalDateTime.now(); } } @PreUpdate protected void onUpdate() { updatedAt = LocalDateTime.now(); } public enum DocumentType { SalesOrder, SalesInvoice, PurchaseOrder, PurchaseInvoice, ReturnOrder, ReturnInvoice } public enum TransactionStatus { TEMPORARY, SAVED, POSTED, COMMITTED, CANCELLED, ADJUSTED, VOIDED } }

Address Entity

import lombok.Data; import javax.persistence.*; @Entity @Table(name = "addresses") @Data public class Address { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "line1") private String line1; @Column(name = "line2") private String line2; @Column(name = "line3") private String line3; @Column(name = "city") private String city; @Column(name = "region") private String region; // State/Province @Column(name = "country") private String country = "US"; @Column(name = "postal_code") private String postalCode; @Column(name = "latitude") private Double latitude; @Column(name = "longitude") private Double longitude; @Column(name = "address_type") @Enumerated(EnumType.STRING) private AddressType addressType; @Column(name = "created_at") private LocalDateTime createdAt; @PrePersist protected void onCreate() { createdAt = LocalDateTime.now(); } public enum AddressType { ORIGIN, DESTINATION, BILLING, SHIPPING, LOCATION } }

Tax Line Entity

import lombok.Data; import javax.persistence.*; import java.math.BigDecimal; @Entity @Table(name = "tax_lines") @Data public class TaxLine { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "transaction_id") private TaxTransaction transaction; @Column(name = "line_number") private String lineNumber; @Column(name = "item_code") private String itemCode; @Column(name = "item_description") private String itemDescription; @Column(name = "quantity", precision = 10, scale = 4) private BigDecimal quantity; @Column(name = "amount", precision = 15, scale = 4) private BigDecimal amount; @Column(name = "tax_amount", precision = 15, scale = 4) private BigDecimal taxAmount; @Column(name = "tax_code") private String taxCode; @Column(name = "discount_amount", precision = 15, scale = 4) private BigDecimal discountAmount; @Column(name = "tax_included") private Boolean taxIncluded = false; @Column(name = "ref1") private String ref1; @Column(name = "ref2") private String ref2; @Column(name = "created_at") private LocalDateTime createdAt; @PrePersist protected void onCreate() { createdAt = LocalDateTime.now(); } }

4. Data Transfer Objects (DTOs)

Tax Calculation Request DTO

import lombok.Data; import javax.validation.constraints.*; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.List; @Data public class TaxCalculationRequestDTO { @NotBlank(message = "Transaction code is required") private String transactionCode; @NotNull(message = "Document type is required") private String documentType; // SalesInvoice, SalesOrder, etc. private LocalDateTime transactionDate; @NotBlank(message = "Customer code is required") private String customerCode; private String companyCode = "DEFAULT"; @NotNull(message = "Origin address is required") private AddressDTO originAddress; @NotNull(message = "Destination address is required") private AddressDTO destinationAddress; @NotNull(message = "Lines are required") @Size(min = 1, message = "At least one line item is required") private List<LineItemDTO> lines; private String currencyCode = "USD"; private Boolean commit = false; private String customerUsageType; private String businessIdentificationNo; private String purchaseOrderNo; private String referenceCode; @Data public static class AddressDTO { @NotBlank(message = "Address line1 is required") private String line1; private String line2; private String line3; @NotBlank(message = "City is required") private String city; @NotBlank(message = "Region is required") private String region; // State/Province @NotBlank(message = "Country is required") private String country = "US"; @NotBlank(message = "Postal code is required") private String postalCode; private Double latitude; private Double longitude; } @Data public static class LineItemDTO { @NotBlank(message = "Line number is required") private String lineNumber; @NotBlank(message = "Item code is required") private String itemCode; private String itemDescription; @NotNull(message = "Quantity is required") @DecimalMin(value = "0.0", inclusive = false, message = "Quantity must be greater than 0") private BigDecimal quantity = BigDecimal.ONE; @NotNull(message = "Amount is required") @DecimalMin(value = "0.0", message = "Amount must be non-negative") private BigDecimal amount; private String taxCode; private Boolean taxIncluded = false; private BigDecimal discountAmount = BigDecimal.ZERO; private String ref1; private String ref2; } }

Tax Calculation Response DTO

import lombok.Data; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.List; @Data public class TaxCalculationResponseDTO { private String transactionCode; private String avalaraTransactionId; private String documentType; private LocalDateTime transactionDate; private BigDecimal totalAmount; private BigDecimal totalTax; private BigDecimal taxableAmount; private BigDecimal exemptAmount; private String currencyCode; private List<TaxLineDTO> lines; private List<TaxSummaryDTO> taxSummary; private List<TaxAddressDTO> addresses; private String status; private Boolean commitStatus; private LocalDateTime taxCalculationDate; @Data public static class TaxLineDTO { private String lineNumber; private String itemCode; private String itemDescription; private BigDecimal quantity; private BigDecimal amount; private BigDecimal taxAmount; private String taxCode; private BigDecimal discountAmount; private Boolean taxIncluded; private List<TaxDetailDTO> taxDetails; } @Data public static class TaxDetailDTO { private String jurisdictionName; private String jurisdictionType; private BigDecimal rate; private BigDecimal taxAmount; private BigDecimal taxableAmount; private String taxName; private String taxType; } @Data public static class TaxSummaryDTO { private String country; private String region; private String jurisdictionType; private String taxName; private BigDecimal rate; private BigDecimal taxAmount; private BigDecimal taxableAmount; } @Data public static class TaxAddressDTO { private String addressType; private String line1; private String line2; private String line3; private String city; private String region; private String country; private String postalCode; } }

Address Validation DTO

import lombok.Data; import javax.validation.constraints.NotBlank; @Data public class AddressValidationRequestDTO { @NotBlank(message = "Address line1 is required") private String line1; private String line2; private String line3; @NotBlank(message = "City is required") private String city; @NotBlank(message = "Region is required") private String region; @NotBlank(message = "Country is required") private String country = "US"; @NotBlank(message = "Postal code is required") private String postalCode; private String textCase = "Mixed"; // Upper, Mixed private Double latitude; private Double longitude; }

5. Avalara API Client Service

Main Avalara Service

import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import okhttp3.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import java.io.IOException; import java.util.Base64; import java.util.Map; import java.util.concurrent.TimeUnit; @Slf4j @Service public class AvalaraService { private final AvalaraConfig config; private final OkHttpClient httpClient; private final ObjectMapper objectMapper; @Autowired public AvalaraService(AvalaraConfig config, ObjectMapper objectMapper) { this.config = config; this.objectMapper = objectMapper; this.httpClient = new OkHttpClient.Builder() .connectTimeout(config.getTimeout(), TimeUnit.MILLISECONDS) .readTimeout(config.getTimeout(), TimeUnit.MILLISECONDS) .writeTimeout(config.getTimeout(), TimeUnit.MILLISECONDS) .addInterceptor(new AuthInterceptor(config.getAccountId(), config.getLicenseKey())) .addInterceptor(new UserAgentInterceptor(config)) .addInterceptor(new RetryInterceptor(config.getMaxRetries(), 1000)) .build(); } /** * Create or adjust a transaction */ public Map<String, Object> createTransaction(Map<String, Object> transactionRequest) throws AvalaraException { try { String jsonBody = objectMapper.writeValueAsString(transactionRequest); String url = config.getBaseUrl() + "/api/v2/transactions/create"; RequestBody body = RequestBody.create( jsonBody, MediaType.parse("application/json") ); Request request = new Request.Builder() .url(url) .post(body) .build(); String responseBody = executeRequest(request); return objectMapper.readValue(responseBody, new TypeReference<Map<String, Object>>() {}); } catch (IOException e) { log.error("Error creating transaction: {}", e.getMessage(), e); throw new AvalaraException("Failed to create transaction", e); } } /** * Calculate tax for a transaction */ public Map<String, Object> calculateTax(Map<String, Object> taxRequest) throws AvalaraException { try { String jsonBody = objectMapper.writeValueAsString(taxRequest); String url = config.getBaseUrl() + "/api/v2/transactions/create"; // For calculation only, don't save the transaction url += "?$include=Lines,Details,Summary,Addresses"; RequestBody body = RequestBody.create( jsonBody, MediaType.parse("application/json") ); Request request = new Request.Builder() .url(url) .post(body) .build(); String responseBody = executeRequest(request); return objectMapper.readValue(responseBody, new TypeReference<Map<String, Object>>() {}); } catch (IOException e) { log.error("Error calculating tax: {}", e.getMessage(), e); throw new AvalaraException("Failed to calculate tax", e); } } /** * Validate an address */ @Cacheable(value = "addressValidation", key = "#addressRequest.hashCode()") public Map<String, Object> validateAddress(Map<String, Object> addressRequest) throws AvalaraException { try { String jsonBody = objectMapper.writeValueAsString(addressRequest); String url = config.getBaseUrl() + "/api/v2/addresses/resolve"; RequestBody body = RequestBody.create( jsonBody, MediaType.parse("application/json") ); Request request = new Request.Builder() .url(url) .post(body) .build(); String responseBody = executeRequest(request); return objectMapper.readValue(responseBody, new TypeReference<Map<String, Object>>() {}); } catch (IOException e) { log.error("Error validating address: {}", e.getMessage(), e); throw new AvalaraException("Failed to validate address", e); } } /** * Get tax rates by postal code */ @Cacheable(value = "taxRates", key = "#postalCode + '_' + #country + '_' + #line1") public Map<String, Object> getTaxRatesByPostalCode(String postalCode, String country, String line1) throws AvalaraException { try { HttpUrl.Builder urlBuilder = HttpUrl.parse(config.getBaseUrl() + "/api/v2/taxrates/bypostalcode").newBuilder(); urlBuilder.addQueryParameter("postalCode", postalCode); urlBuilder.addQueryParameter("country", country); if (line1 != null) { urlBuilder.addQueryParameter("line1", line1); } Request request = new Request.Builder() .url(urlBuilder.build()) .get() .build(); String responseBody = executeRequest(request); return objectMapper.readValue(responseBody, new TypeReference<Map<String, Object>>() {}); } catch (IOException e) { log.error("Error getting tax rates: {}", e.getMessage(), e); throw new AvalaraException("Failed to get tax rates", e); } } /** * Void a transaction */ public Map<String, Object> voidTransaction(String companyCode, String transactionCode, String documentType) throws AvalaraException { try { String url = config.getBaseUrl() + "/api/v2/companies/" + companyCode + "/transactions/" + transactionCode + "/void"; Map<String, String> voidRequest = Map.of( "code", "DocVoided" ); String jsonBody = objectMapper.writeValueAsString(voidRequest); RequestBody body = RequestBody.create( jsonBody, MediaType.parse("application/json") ); Request request = new Request.Builder() .url(url) .post(body) .build(); String responseBody = executeRequest(request); return objectMapper.readValue(responseBody, new TypeReference<Map<String, Object>>() {}); } catch (IOException e) { log.error("Error voiding transaction: {}", e.getMessage(), e); throw new AvalaraException("Failed to void transaction", e); } } /** * Get transaction by ID */ public Map<String, Object> getTransaction(String companyCode, String transactionCode) throws AvalaraException { try { String url = config.getBaseUrl() + "/api/v2/companies/" + companyCode + "/transactions/" + transactionCode + "?$include=Lines,Details,Summary,Addresses"; Request request = new Request.Builder() .url(url) .get() .build(); String responseBody = executeRequest(request); return objectMapper.readValue(responseBody, new TypeReference<Map<String, Object>>() {}); } catch (IOException e) { log.error("Error getting transaction: {}", e.getMessage(), e); throw new AvalaraException("Failed to get transaction", e); } } /** * Create a refund transaction */ public Map<String, Object> createRefund(String companyCode, String originalTransactionCode, Map<String, Object> refundRequest) throws AvalaraException { try { String url = config.getBaseUrl() + "/api/v2/companies/" + companyCode + "/transactions/" + originalTransactionCode + "/refund"; String jsonBody = objectMapper.writeValueAsString(refundRequest); RequestBody body = RequestBody.create( jsonBody, MediaType.parse("application/json") ); Request request = new Request.Builder() .url(url) .post(body) .build(); String responseBody = executeRequest(request); return objectMapper.readValue(responseBody, new TypeReference<Map<String, Object>>() {}); } catch (IOException e) { log.error("Error creating refund: {}", e.getMessage(), e); throw new AvalaraException("Failed to create refund", e); } } /** * Ping Avalara service to check connectivity */ public Map<String, Object> ping() throws AvalaraException { try { Request request = new Request.Builder() .url(config.getBaseUrl() + "/api/v2/utilities/ping") .get() .build(); String responseBody = executeRequest(request); return objectMapper.readValue(responseBody, new TypeReference<Map<String, Object>>() {}); } catch (IOException e) { log.error("Error pinging Avalara: {}", e.getMessage(), e); throw new AvalaraException("Failed to ping Avalara service", e); } } private String executeRequest(Request request) throws IOException, AvalaraException { try (Response response = httpClient.newCall(request).execute()) { if (!response.isSuccessful()) { String errorBody = response.body().string(); log.error("Avalara API error: {} - {}", response.code(), errorBody); throw new AvalaraException("Avalara API error: " + response.code() + " - " + errorBody); } return response.body().string(); } } }

Authentication Interceptor

import okhttp3.*; import org.jetbrains.annotations.NotNull; import java.io.IOException; import java.util.Base64; public class AuthInterceptor implements Interceptor { private final String credentials; public AuthInterceptor(String accountId, String licenseKey) { String auth = accountId + ":" + licenseKey; this.credentials = "Basic " + Base64.getEncoder().encodeToString(auth.getBytes()); } @NotNull @Override public Response intercept(@NotNull Chain chain) throws IOException { Request originalRequest = chain.request(); Request authenticatedRequest = originalRequest.newBuilder() .header("Authorization", credentials) .build(); return chain.proceed(authenticatedRequest); } }

User Agent Interceptor

import okhttp3.*; import org.jetbrains.annotations.NotNull; import java.io.IOException; public class UserAgentInterceptor implements Interceptor { private final String userAgent; public UserAgentInterceptor(AvalaraConfig config) { this.userAgent = String.format("%s/%s (%s; %s; %s)", config.getAppName(), config.getAppVersion(), config.getMachineName(), "Java", config.getEnvironment()); } @NotNull @Override public Response intercept(@NotNull Chain chain) throws IOException { Request originalRequest = chain.request(); Request modifiedRequest = originalRequest.newBuilder() .header("User-Agent", userAgent) .header("X-Avalara-Client", userAgent) .build(); return chain.proceed(modifiedRequest); } }

Retry Interceptor

import okhttp3.*; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.concurrent.TimeUnit; public class RetryInterceptor implements Interceptor { private static final Logger logger = LoggerFactory.getLogger(RetryInterceptor.class); private final int maxRetries; private final long backoffDelay; public RetryInterceptor(int maxRetries, long backoffDelay) { this.maxRetries = maxRetries; this.backoffDelay = backoffDelay; } @NotNull @Override public Response intercept(@NotNull Chain chain) throws IOException { Request request = chain.request(); Response response = null; IOException exception = null; for (int attempt = 1; attempt <= maxRetries; attempt++) { try { response = chain.proceed(request); if (response.isSuccessful()) { return response; } // Only retry on server errors (5xx) or rate limiting if (response.code() < 500 && response.code() != 429) { return response; } logger.warn("Avalara API request failed with status {} on attempt {}/{}", response.code(), attempt, maxRetries); } catch (IOException e) { exception = e; logger.warn("Avalara API request failed with IOException on attempt {}/{}: {}", attempt, maxRetries, e.getMessage()); } if (attempt < maxRetries) { try { long delay = backoffDelay * (long) Math.pow(2, attempt - 1); TimeUnit.MILLISECONDS.sleep(delay); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new IOException("Request interrupted", e); } } if (response != null) { response.close(); } } if (exception != null) { throw exception; } return response; } }

6. Tax Processing Service

import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.*; @Slf4j @Service public class TaxProcessingService { @Autowired private AvalaraService avalaraService; @Autowired private TaxTransactionRepository taxTransactionRepository; @Autowired private AddressRepository addressRepository; @Autowired private TaxLineRepository taxLineRepository; @Autowired private ObjectMapper objectMapper; /** * Calculate tax for a transaction */ @Transactional public TaxCalculationResponseDTO calculateTax(TaxCalculationRequestDTO request) throws AvalaraException { try { // Prepare Avalara transaction request Map<String, Object> avalaraRequest = prepareAvalaraTransactionRequest(request, false); // Call Avalara API Map<String, Object> avalaraResponse = avalaraService.calculateTax(avalaraRequest); // Convert response to DTO TaxCalculationResponseDTO response = convertToTaxResponseDTO(avalaraResponse); log.info("Tax calculation completed for transaction: {}", request.getTransactionCode()); return response; } catch (Exception e) { log.error("Error calculating tax for transaction {}: {}", request.getTransactionCode(), e.getMessage(), e); throw new AvalaraException("Tax calculation failed: " + e.getMessage(), e); } } /** * Create and commit a tax transaction */ @Transactional public TaxCalculationResponseDTO createTransaction(TaxCalculationRequestDTO request) throws AvalaraException { try { // Prepare Avalara transaction request Map<String, Object> avalaraRequest = prepareAvalaraTransactionRequest(request, true); // Call Avalara API Map<String, Object> avalaraResponse = avalaraService.createTransaction(avalaraRequest); // Save transaction to database saveTransactionToDatabase(request, avalaraResponse); // Convert response to DTO TaxCalculationResponseDTO response = convertToTaxResponseDTO(avalaraResponse); log.info("Tax transaction created and committed: {}", request.getTransactionCode()); return response; } catch (Exception e) { log.error("Error creating tax transaction {}: {}", request.getTransactionCode(), e.getMessage(), e); throw new AvalaraException("Transaction creation failed: " + e.getMessage(), e); } } /** * Validate address using Avalara */ public Map<String, Object> validateAddress(AddressValidationRequestDTO request) throws AvalaraException { try { Map<String, Object> addressRequest = new HashMap<>(); addressRequest.put("line1", request.getLine1()); addressRequest.put("line2", request.getLine2()); addressRequest.put("line3", request.getLine3()); addressRequest.put("city", request.getCity()); addressRequest.put("region", request.getRegion()); addressRequest.put("country", request.getCountry()); addressRequest.put("postalCode", request.getPostalCode()); addressRequest.put("textCase", request.getTextCase()); if (request.getLatitude() != null && request.getLongitude() != null) { addressRequest.put("latitude", request.getLatitude()); addressRequest.put("longitude", request.getLongitude()); } return avalaraService.validateAddress(addressRequest); } catch (Exception e) { log.error("Error validating address: {}", e.getMessage(), e); throw new AvalaraException("Address validation failed: " + e.getMessage(), e); } } /** * Void a tax transaction */ @Transactional public Map<String, Object> voidTransaction(String companyCode, String transactionCode) throws AvalaraException { try { Map<String, Object> voidResponse = avalaraService.voidTransaction(companyCode, transactionCode, "SalesInvoice"); // Update transaction status in database taxTransactionRepository.findByTransactionCode(transactionCode).ifPresent(transaction -> { transaction.setStatus(TaxTransaction.TransactionStatus.VOIDED); taxTransactionRepository.save(transaction); }); log.info("Transaction voided: {}", transactionCode); return voidResponse; } catch (Exception e) { log.error("Error voiding transaction {}: {}", transactionCode, e.getMessage(), e); throw new AvalaraException("Transaction voiding failed: " + e.getMessage(), e); } } /** * Prepare Avalara transaction request */ private Map<String, Object> prepareAvalaraTransactionRequest(TaxCalculationRequestDTO request, boolean commit) { Map<String, Object> avalaraRequest = new HashMap<>(); // Basic transaction info avalaraRequest.put("code", request.getTransactionCode()); avalaraRequest.put("date", request.getTransactionDate() != null ? request.getTransactionDate().toString() : LocalDateTime.now().toString()); avalaraRequest.put("customerCode", request.getCustomerCode()); avalaraRequest.put("companyCode", request.getCompanyCode()); avalaraRequest.put("type", request.getDocumentType()); avalaraRequest.put("currencyCode", request.getCurrencyCode()); avalaraRequest.put("commit", commit); // Addresses Map<String, Object> addresses = new HashMap<>(); addresses.put("singleLocation", convertAddressToMap(request.getDestinationAddress())); if (request.getOriginAddress() != null) { Map<String, Object> addressMap = new HashMap<>(); addressMap.put("shipFrom", convertAddressToMap(request.getOriginAddress())); addressMap.put("shipTo", convertAddressToMap(request.getDestinationAddress())); avalaraRequest.put("addresses", addressMap); } else { avalaraRequest.put("addresses", addresses); } // Lines List<Map<String, Object>> lines = new ArrayList<>(); for (TaxCalculationRequestDTO.LineItemDTO line : request.getLines()) { Map<String, Object> lineMap = new HashMap<>(); lineMap.put("number", line.getLineNumber()); lineMap.put("itemCode", line.getItemCode()); lineMap.put("description", line.getItemDescription()); lineMap.put("quantity", line.getQuantity()); lineMap.put("amount", line.getAmount()); lineMap.put("taxCode", line.getTaxCode()); lineMap.put("taxIncluded", line.getTaxIncluded()); lineMap.put("discountAmount", line.getDiscountAmount()); if (line.getRef1() != null) lineMap.put("ref1", line.getRef1()); if (line.getRef2() != null) lineMap.put("Ref2", line.getRef2()); lines.add(lineMap); } avalaraRequest.put("lines", lines); // Additional fields if (request.getCustomerUsageType() != null) { avalaraRequest.put("customerUsageType", request.getCustomerUsageType()); } if (request.getBusinessIdentificationNo() != null) { avalaraRequest.put("businessIdentificationNo", request.getBusinessIdentificationNo()); } if (request.getPurchaseOrderNo() != null) { avalaraRequest.put("purchaseOrderNo", request.getPurchaseOrderNo()); } if (request.getReferenceCode() != null) { avalaraRequest.put("referenceCode", request.getReferenceCode()); } return avalaraRequest; } /** * Convert address DTO to map */ private Map<String, Object> convertAddressToMap(TaxCalculationRequestDTO.AddressDTO address) { Map<String, Object> addressMap = new HashMap<>(); addressMap.put("line1", address.getLine1()); if (address.getLine2() != null) addressMap.put("line2", address.getLine2()); if (address.getLine3() != null) addressMap.put("line3", address.getLine3()); addressMap.put("city", address.getCity()); addressMap.put("region", address.getRegion()); addressMap.put("country", address.getCountry()); addressMap.put("postalCode", address.getPostalCode()); if (address.getLatitude() != null) addressMap.put("latitude", address.getLatitude()); if (address.getLongitude() != null) addressMap.put("longitude", address.getLongitude()); return addressMap; } /** * Convert Avalara response to DTO */ @SuppressWarnings("unchecked") private TaxCalculationResponseDTO convertToTaxResponseDTO(Map<String, Object> avalaraResponse) { TaxCalculationResponseDTO response = new TaxCalculationResponseDTO(); response.setTransactionCode((String) avalaraResponse.get("code")); response.setAvalaraTransactionId((String) avalaraResponse.get("id")); response.setDocumentType((String) avalaraResponse.get("type")); response.setTotalAmount(convertToBigDecimal(avalaraResponse.get("totalAmount"))); response.setTotalTax(convertToBigDecimal(avalaraResponse.get("totalTax"))); response.setTaxableAmount(convertToBigDecimal(avalaraResponse.get("taxableAmount"))); response.setExemptAmount(convertToBigDecimal(avalaraResponse.get("exemptAmount"))); response.setCurrencyCode((String) avalaraResponse.get("currencyCode")); response.setStatus((String) avalaraResponse.get("status")); response.setCommitStatus((Boolean) avalaraResponse.get("committed")); // Parse transaction date if (avalaraResponse.get("date") != null) { response.setTransactionDate(LocalDateTime.parse((String) avalaraResponse.get("date"))); } // Parse lines if (avalaraResponse.get("lines") instanceof List) { List<Map<String, Object>> lines = (List<Map<String, Object>>) avalaraResponse.get("lines"); List<TaxCalculationResponseDTO.TaxLineDTO> taxLines = new ArrayList<>(); for (Map<String, Object> line : lines) { TaxCalculationResponseDTO.TaxLineDTO taxLine = new TaxCalculationResponseDTO.TaxLineDTO(); taxLine.setLineNumber((String) line.get("number")); taxLine.setItemCode((String) line.get("itemCode")); taxLine.setItemDescription((String) line.get("description")); taxLine.setQuantity(convertToBigDecimal(line.get("quantity"))); taxLine.setAmount(convertToBigDecimal(line.get("amount"))); taxLine.setTaxAmount(convertToBigDecimal(line.get("tax"))); taxLine.setTaxCode((String) line.get("taxCode")); taxLine.setDiscountAmount(convertToBigDecimal(line.get("discountAmount"))); taxLine.setTaxIncluded((Boolean) line.get("taxIncluded")); // Parse tax details if (line.get("details") instanceof List) { List<Map<String, Object>> details = (List<Map<String, Object>>) line.get("details"); List<TaxCalculationResponseDTO.TaxDetailDTO> taxDetails = new ArrayList<>(); for (Map<String, Object> detail : details) { TaxCalculationResponseDTO.TaxDetailDTO taxDetail = new TaxCalculationResponseDTO.TaxDetailDTO(); taxDetail.setJurisdictionName((String) detail.get("jurisdictionName")); taxDetail.setJurisdictionType((String) detail.get("jurisdictionType")); taxDetail.setRate(convertToBigDecimal(detail.get("rate"))); taxDetail.setTaxAmount(convertToBigDecimal(detail.get("tax"))); taxDetail.setTaxableAmount(convertToBigDecimal(detail.get("taxable"))); taxDetail.setTaxName((String) detail.get("taxName")); taxDetail.setTaxType((String) detail.get("taxType")); taxDetails.add(taxDetail); } taxLine.setTaxDetails(taxDetails); } taxLines.add(taxLine); } response.setLines(taxLines); } return response; } /** * Save transaction to database */ private void saveTransactionToDatabase(TaxCalculationRequestDTO request, Map<String, Object> avalaraResponse) { try { // Save transaction TaxTransaction transaction = new TaxTransaction(); transaction.setTransactionCode(request.getTransactionCode()); transaction.setAvalaraTransactionId((String) avalaraResponse.get("id")); transaction.setDocumentType(TaxTransaction.DocumentType.valueOf((String) avalaraResponse.get("type"))); transaction.setCustomerCode(request.getCustomerCode()); transaction.setCompanyCode(request.getCompanyCode()); transaction.setTotalAmount(convertToBigDecimal(avalaraResponse.get("totalAmount"))); transaction.setTotalTax(convertToBigDecimal(avalaraResponse.get("totalTax"))); transaction.setTaxableAmount(convertToBigDecimal(avalaraResponse.get("taxableAmount"))); transaction.setExemptAmount(convertToBigDecimal(avalaraResponse.get("exemptAmount"))); transaction.setCurrencyCode(request.getCurrencyCode()); transaction.setStatus(TaxTransaction.TransactionStatus.COMMITTED); transaction.setCommitStatus(true); transaction.setTaxCalculationDate(LocalDateTime.now()); taxTransactionRepository.save(transaction); log.info("Tax transaction saved to database: {}", request.getTransactionCode()); } catch (Exception e) { log.error("Error saving transaction to database: {}", e.getMessage(), e); // Don't throw exception to avoid rolling back the Avalara transaction } } /** * Convert object to BigDecimal safely */ private BigDecimal convertToBigDecimal(Object value) { if (value == null) return BigDecimal.ZERO; if (value instanceof BigDecimal) return (BigDecimal) value; if (value instanceof Double) return BigDecimal.valueOf((Double) value); if (value instanceof Integer) return BigDecimal.valueOf((Integer) value); if (value instanceof String) return new BigDecimal((String) value); return BigDecimal.ZERO; } }

7. REST Controllers

Tax Controller

import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; import java.util.HashMap; import java.util.Map; @Slf4j @RestController @RequestMapping("/api/tax") public class TaxController { @Autowired private TaxProcessingService taxProcessingService; @Autowired private AvalaraService avalaraService; /** * Calculate tax for a transaction */ @PostMapping("/calculate") public ResponseEntity<?> calculateTax(@Valid @RequestBody TaxCalculationRequestDTO request) { try { TaxCalculationResponseDTO response = taxProcessingService.calculateTax(request); return ResponseEntity.ok(response); } catch (AvalaraException e) { log.error("Tax calculation error: {}", e.getMessage(), e); Map<String, Object> errorResponse = new HashMap<>(); errorResponse.put("error", true); errorResponse.put("message", "Tax calculation failed"); errorResponse.put("error_description", e.getMessage()); return ResponseEntity.badRequest().body(errorResponse); } } /** * Create and commit a tax transaction */ @PostMapping("/transactions") public ResponseEntity<?> createTransaction(@Valid @RequestBody TaxCalculationRequestDTO request) { try { TaxCalculationResponseDTO response = taxProcessingService.createTransaction(request); return ResponseEntity.ok(response); } catch (AvalaraException e) { log.error("Transaction creation error: {}", e.getMessage(), e); Map<String, Object> errorResponse = new HashMap<>(); errorResponse.put("error", true); errorResponse.put("message", "Transaction creation failed"); errorResponse.put("error_description", e.getMessage()); return ResponseEntity.badRequest().body(errorResponse); } } /** * Validate address */ @PostMapping("/addresses/validate") public ResponseEntity<?> validateAddress(@Valid @RequestBody AddressValidationRequestDTO request) { try { Map<String, Object> response = taxProcessingService.validateAddress(request); return ResponseEntity.ok(response); } catch (AvalaraException e) { log.error("Address validation error: {}", e.getMessage(), e); Map<String, Object> errorResponse = new HashMap<>(); errorResponse.put("error", true); errorResponse.put("message", "Address validation failed"); errorResponse.put("error_description", e.getMessage()); return ResponseEntity.badRequest().body(errorResponse); } } /** * Get tax rates by postal code */ @GetMapping("/rates") public ResponseEntity<?> getTaxRates( @RequestParam String postalCode, @RequestParam String country, @RequestParam(required = false) String line1) { try { Map<String, Object> response = avalaraService.getTaxRatesByPostalCode(postalCode, country, line1); return ResponseEntity.ok(response); } catch (AvalaraException e) { log.error("Tax rates lookup error: {}", e.getMessage(), e); Map<String, Object> errorResponse = new HashMap<>(); errorResponse.put("error", true); errorResponse.put("message", "Tax rates lookup failed"); errorResponse.put("error_description", e.getMessage()); return ResponseEntity.badRequest().body(errorResponse); } } /** * Void a transaction */ @PostMapping("/transactions/{transactionCode}/void") public ResponseEntity<?> voidTransaction( @PathVariable String transactionCode, @RequestParam(defaultValue = "DEFAULT") String companyCode) { try { Map<String, Object> response = taxProcessingService.voidTransaction(companyCode, transactionCode); return ResponseEntity.ok(response); } catch (AvalaraException e) { log.error("Transaction voiding error: {}", e.getMessage(), e); Map<String, Object> errorResponse = new HashMap<>(); errorResponse.put("error", true); errorResponse.put("message", "Transaction voiding failed"); errorResponse.put("error_description", e.getMessage()); return ResponseEntity.badRequest().body(errorResponse); } } /** * Ping Avalara service */ @GetMapping("/ping") public ResponseEntity<?> ping() { try { Map<String, Object> response = avalaraService.ping(); return ResponseEntity.ok(response); } catch (AvalaraException e) { log.error("Avalara ping error: {}", e.getMessage(), e); Map<String, Object> errorResponse = new HashMap<>(); errorResponse.put("error", true); errorResponse.put("message", "Avalara service unavailable"); errorResponse.put("error_description", e.getMessage()); return ResponseEntity.status(503).body(errorResponse); } } }

8. Custom Exception

public class AvalaraException extends Exception { public AvalaraException(String message) { super(message); } public AvalaraException(String message, Throwable cause) { super(message, cause); } }

9. Repository Interfaces

import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @Repository public interface TaxTransactionRepository extends JpaRepository<TaxTransaction, Long> { Optional<TaxTransaction> findByTransactionCode(String transactionCode); Optional<TaxTransaction> findByAvalaraTransactionId(String avalaraTransactionId); List<TaxTransaction> findByCustomerCode(String customerCode); List<TaxTransaction> findByCompanyCode(String companyCode); List<TaxTransaction> findByStatus(TaxTransaction.TransactionStatus status); List<TaxTransaction> findByTransactionDateBetween(LocalDateTime start, LocalDateTime end); @Query("SELECT SUM(t.totalTax) FROM TaxTransaction t WHERE t.status = 'COMMITTED' AND t.transactionDate BETWEEN :start AND :end") BigDecimal getTotalTaxByPeriod(@Param("start") LocalDateTime start, @Param("end") LocalDateTime end); @Query("SELECT COUNT(t) FROM TaxTransaction t WHERE t.status = 'COMMITTED' AND t.transactionDate BETWEEN :start AND :end") Long getTransactionCountByPeriod(@Param("start") LocalDateTime start, @Param("end") LocalDateTime end); } @Repository public interface AddressRepository extends JpaRepository<Address, Long> { List<Address> findByCountry(String country); List<Address> findByRegion(String region); List<Address> findByPostalCode(String postalCode); List<Address> findByAddressType(Address.AddressType addressType); } @Repository public interface TaxLineRepository extends JpaRepository<TaxLine, Long> { List<TaxLine> findByTransaction(TaxTransaction transaction); List<TaxLine> findByItemCode(String itemCode); List<TaxLine> findByTaxCode(String taxCode); }

This comprehensive Avalara Tax API integration provides complete tax calculation, address validation, transaction management, and compliance features for your Java application.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper