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