Stripe Tax Integration in Java: Complete Tax Compliance Solution

Stripe Tax is a comprehensive solution that automatically calculates and collects sales tax, VAT, and GST globally. This guide covers complete integration with Java applications.


Overview of Stripe Tax

Key Features:

  • Automatic Tax Calculation: Real-time tax calculations for 100+ countries
  • Tax Registration Management: Track registration requirements
  • Evidence Collection: Automatically gather proof of location
  • Tax Reporting: Simplified reporting and filing
  • Multi-Tax Support: VAT, GST, Sales Tax, and more

Supported Tax Types:

  • 🇺🇸 US & Canada: Sales tax
  • 🇪🇺 European Union: VAT
  • 🇬🇧 UK: VAT
  • 🇦🇺 Australia & New Zealand: GST
  • 🇸🇬 Singapore: GST
  • 🇦🇪 UAE: VAT
  • 🇸🇦 Saudi Arabia: VAT

Setup and Dependencies

1. Maven Dependencies
<properties>
<stripe-java.version>24.0.0</stripe-java.version>
<spring-boot.version>3.1.0</spring-boot.version>
<jackson.version>2.15.2</jackson.version>
</properties>
<dependencies>
<!-- Stripe Java SDK -->
<dependency>
<groupId>com.stripe</groupId>
<artifactId>stripe-java</artifactId>
<version>${stripe-java.version}</version>
</dependency>
<!-- 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>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- Database (PostgreSQL) -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
2. Stripe Configuration
@Configuration
@ConfigurationProperties(prefix = "stripe")
@Data
public class StripeConfig {
private String secretKey;
private String publishableKey;
private String webhookSecret;
private String apiVersion = "2023-10-16";
@PostConstruct
public void init() {
Stripe.apiKey = this.secretKey;
Stripe.setMaxNetworkRetries(2);
}
}
# application.yml
stripe:
secret-key: ${STRIPE_SECRET_KEY:sk_test_xxx}
publishable-key: ${STRIPE_PUBLISHABLE_KEY:pk_test_xxx}
webhook-secret: ${STRIPE_WEBHOOK_SECRET:whsec_xxx}
api-version: "2023-10-16"

Core Implementation

1. Tax Calculation Service
@Service
@Slf4j
public class StripeTaxService {
private final StripeConfig stripeConfig;
private final ObjectMapper objectMapper;
public StripeTaxService(StripeConfig stripeConfig, ObjectMapper objectMapper) {
this.stripeConfig = stripeConfig;
this.objectMapper = objectMapper;
}
public CalculationLineItemsResponse calculateTax(CalculateTaxRequest request) {
try {
Map<String, Object> params = new HashMap<>();
params.put("currency", request.getCurrency());
params.put("customer", request.getCustomerId());
params.put("customer_details", createCustomerDetails(request));
params.put("line_items", createLineItems(request.getLineItems()));
params.put("shipping_cost", createShippingCost(request.getShippingCost()));
TaxCalculation calculation = TaxCalculation.create(params);
return mapToCalculationResponse(calculation);
} catch (StripeException e) {
log.error("Failed to calculate tax", e);
throw new StripeTaxException("Tax calculation failed", e);
}
}
private Map<String, Object> createCustomerDetails(CalculateTaxRequest request) {
Map<String, Object> customerDetails = new HashMap<>();
if (request.getCustomerAddress() != null) {
customerDetails.put("address", mapAddress(request.getCustomerAddress()));
customerDetails.put("address_source", "billing");
}
if (request.getCustomerIpAddress() != null) {
customerDetails.put("ip_address", request.getCustomerIpAddress());
}
return customerDetails;
}
private List<Map<String, Object>> createLineItems(List<LineItemRequest> lineItems) {
return lineItems.stream().map(item -> {
Map<String, Object> lineItem = new HashMap<>();
lineItem.put("amount", item.getAmount());
lineItem.put("reference", item.getReference());
if (item.getProduct() != null) {
lineItem.put("product", item.getProduct());
}
if (item.getTaxCode() != null) {
lineItem.put("tax_code", item.getTaxCode());
}
return lineItem;
}).collect(Collectors.toList());
}
private Map<String, Object> createShippingCost(ShippingCostRequest shipping) {
if (shipping == null) return null;
Map<String, Object> shippingCost = new HashMap<>();
shippingCost.put("amount", shipping.getAmount());
if (shipping.getTaxCode() != null) {
shippingCost.put("tax_code", shipping.getTaxCode());
}
return shippingCost;
}
private Map<String, Object> mapAddress(Address address) {
Map<String, Object> stripeAddress = new HashMap<>();
stripeAddress.put("line1", address.getLine1());
stripeAddress.put("line2", address.getLine2());
stripeAddress.put("city", address.getCity());
stripeAddress.put("state", address.getState());
stripeAddress.put("postal_code", address.getPostalCode());
stripeAddress.put("country", address.getCountry());
return stripeAddress;
}
private CalculationLineItemsResponse mapToCalculationResponse(TaxCalculation calculation) {
CalculationLineItemsResponse response = new CalculationLineItemsResponse();
response.setTaxCalculationId(calculation.getId());
response.setCurrency(calculation.getCurrency());
response.setTaxAmount(calculation.getTaxAmountExclusive());
response.setTotalAmount(calculation.getAmountTotal());
// Map line items with tax details
List<LineItemTaxResponse> lineItems = calculation.getLineItems().getData().stream()
.map(this::mapLineItem)
.collect(Collectors.toList());
response.setLineItems(lineItems);
// Map tax breakdown
List<TaxRateResponse> taxBreakdown = calculation.getTaxBreakdown().stream()
.map(this::mapTaxBreakdown)
.collect(Collectors.toList());
response.setTaxBreakdown(taxBreakdown);
return response;
}
private LineItemTaxResponse mapLineItem(TaxCalculation.LineItem stripeLineItem) {
LineItemTaxResponse item = new LineItemTaxResponse();
item.setAmount(stripeLineItem.getAmount());
item.setTaxAmount(stripeLineItem.getAmountTax());
item.setTaxCode(stripeLineItem.getTaxCode());
return item;
}
private TaxRateResponse mapTaxBreakdown(TaxCalculation.TaxBreakdown breakdown) {
TaxRateResponse taxRate = new TaxRateResponse();
taxRate.setJurisdiction(breakdown.getJurisdiction().getCountry());
taxRate.setTaxRate(breakdown.getTaxRateDetails().getPercentageDecimal());
taxRate.setTaxAmount(breakdown.getAmount());
return taxRate;
}
}
2. Domain Models
@Data
public class CalculateTaxRequest {
@NotBlank
private String currency;
private String customerId;
private Address customerAddress;
private String customerIpAddress;
@NotEmpty
private List<LineItemRequest> lineItems;
private ShippingCostRequest shippingCost;
}
@Data
public class LineItemRequest {
@NotNull
private Long amount;
@NotBlank
private String reference;
private String product;
private String taxCode; // e.g., "txcd_10000000" for general goods
}
@Data
public class Address {
private String line1;
private String line2;
private String city;
private String state;
private String postalCode;
@NotBlank
private String country; // ISO 3166-1 alpha-2 code
}
@Data
public class ShippingCostRequest {
@NotNull
private Long amount;
private String taxCode = "txcd_92010001"; // Standard shipping tax code
}
@Data
public class CalculationLineItemsResponse {
private String taxCalculationId;
private String currency;
private Long taxAmount;
private Long totalAmount;
private List<LineItemTaxResponse> lineItems;
private List<TaxRateResponse> taxBreakdown;
}
@Data
public class LineItemTaxResponse {
private String reference;
private Long amount;
private Long taxAmount;
private String taxCode;
}
@Data
public class TaxRateResponse {
private String jurisdiction;
private Double taxRate;
private Long taxAmount;
}
// Custom Exceptions
public class StripeTaxException extends RuntimeException {
public StripeTaxException(String message) {
super(message);
}
public StripeTaxException(String message, Throwable cause) {
super(message, cause);
}
}
3. Database Entities
@Entity
@Table(name = "tax_transactions")
@Data
public class TaxTransaction {
@Id
private String id;
@Column(name = "stripe_calculation_id")
private String stripeCalculationId;
@Column(name = "customer_id")
private String customerId;
@Column(name = "currency")
private String currency;
@Column(name = "subtotal_amount")
private Long subtotalAmount;
@Column(name = "tax_amount")
private Long taxAmount;
@Column(name = "total_amount")
private Long totalAmount;
@Column(name = "customer_country")
private String customerCountry;
@Column(name = "customer_postal_code")
private String customerPostalCode;
@Enumerated(EnumType.STRING)
@Column(name = "status")
private TaxStatus status;
@Column(name = "created_at")
private LocalDateTime createdAt;
@OneToMany(mappedBy = "taxTransaction", cascade = CascadeType.ALL)
private List<TaxLineItem> lineItems = new ArrayList<>();
@PrePersist
protected void onCreate() {
this.createdAt = LocalDateTime.now();
this.status = TaxStatus.CALCULATED;
}
}
@Entity
@Table(name = "tax_line_items")
@Data
public class TaxLineItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "tax_transaction_id")
private TaxTransaction taxTransaction;
@Column(name = "item_reference")
private String itemReference;
@Column(name = "amount")
private Long amount;
@Column(name = "tax_amount")
private Long taxAmount;
@Column(name = "tax_code")
private String taxCode;
@Column(name = "tax_rate")
private Double taxRate;
@Column(name = "jurisdiction")
private String jurisdiction;
}
public enum TaxStatus {
CALCULATED,
APPLIED,
REVERSED,
FAILED
}
4. Repository Layer
@Repository
public interface TaxTransactionRepository extends JpaRepository<TaxTransaction, String> {
List<TaxTransaction> findByCustomerIdAndStatus(String customerId, TaxStatus status);
Optional<TaxTransaction> findByStripeCalculationId(String stripeCalculationId);
@Query("SELECT tt FROM TaxTransaction tt WHERE tt.createdAt BETWEEN :startDate AND :endDate")
List<TaxTransaction> findTransactionsInPeriod(@Param("startDate") LocalDateTime startDate, 
@Param("endDate") LocalDateTime endDate);
@Query("SELECT NEW com.example.tax.reporting.TaxSummary(tt.customerCountry, SUM(tt.taxAmount)) " +
"FROM TaxTransaction tt WHERE tt.status = 'APPLIED' AND tt.createdAt BETWEEN :startDate AND :endDate " +
"GROUP BY tt.customerCountry")
List<TaxSummary> getTaxSummaryByCountry(@Param("startDate") LocalDateTime startDate,
@Param("endDate") LocalDateTime endDate);
}

Payment Integration with Tax

1. Payment Intent with Tax
@Service
@Slf4j
public class PaymentService {
private final StripeTaxService taxService;
private final TaxTransactionRepository taxRepository;
public PaymentService(StripeTaxService taxService, TaxTransactionRepository taxRepository) {
this.taxService = taxService;
this.taxRepository = taxRepository;
}
public PaymentIntentResponse createPaymentIntentWithTax(CreatePaymentIntentRequest request) {
try {
// Step 1: Calculate tax
CalculateTaxRequest taxRequest = createTaxRequest(request);
CalculationLineItemsResponse taxCalculation = taxService.calculateTax(taxRequest);
// Step 2: Save tax calculation
TaxTransaction taxTransaction = saveTaxCalculation(taxCalculation, request);
// Step 3: Create payment intent with tax
Map<String, Object> paymentIntentParams = new HashMap<>();
paymentIntentParams.put("amount", taxCalculation.getTotalAmount());
paymentIntentParams.put("currency", request.getCurrency());
paymentIntentParams.put("customer", request.getCustomerId());
paymentIntentParams.put("automatic_payment_methods", Map.of("enabled", true));
// Add tax calculation to payment intent
paymentIntentParams.put("tax_calculation", taxCalculation.getTaxCalculationId());
PaymentIntent paymentIntent = PaymentIntent.create(paymentIntentParams);
// Update transaction with payment intent ID
taxTransaction.setId(paymentIntent.getId());
taxRepository.save(taxTransaction);
return mapToPaymentIntentResponse(paymentIntent, taxCalculation);
} catch (StripeException e) {
log.error("Failed to create payment intent with tax", e);
throw new StripeTaxException("Payment creation failed", e);
}
}
private CalculateTaxRequest createTaxRequest(CreatePaymentIntentRequest request) {
CalculateTaxRequest taxRequest = new CalculateTaxRequest();
taxRequest.setCurrency(request.getCurrency());
taxRequest.setCustomerId(request.getCustomerId());
taxRequest.setCustomerAddress(request.getCustomerAddress());
taxRequest.setCustomerIpAddress(request.getCustomerIpAddress());
List<LineItemRequest> lineItems = request.getLineItems().stream()
.map(item -> {
LineItemRequest lineItem = new LineItemRequest();
lineItem.setAmount(item.getAmount());
lineItem.setReference(item.getReference());
lineItem.setProduct(item.getProductId());
lineItem.setTaxCode(item.getTaxCode());
return lineItem;
})
.collect(Collectors.toList());
taxRequest.setLineItems(lineItems);
if (request.getShippingCost() != null) {
ShippingCostRequest shipping = new ShippingCostRequest();
shipping.setAmount(request.getShippingCost());
taxRequest.setShippingCost(shipping);
}
return taxRequest;
}
private TaxTransaction saveTaxCalculation(CalculationLineItemsResponse taxCalculation, 
CreatePaymentIntentRequest request) {
TaxTransaction transaction = new TaxTransaction();
transaction.setStripeCalculationId(taxCalculation.getTaxCalculationId());
transaction.setCustomerId(request.getCustomerId());
transaction.setCurrency(taxCalculation.getCurrency());
transaction.setSubtotalAmount(calculateSubtotal(taxCalculation));
transaction.setTaxAmount(taxCalculation.getTaxAmount());
transaction.setTotalAmount(taxCalculation.getTotalAmount());
if (request.getCustomerAddress() != null) {
transaction.setCustomerCountry(request.getCustomerAddress().getCountry());
transaction.setCustomerPostalCode(request.getCustomerAddress().getPostalCode());
}
// Save line items
List<TaxLineItem> lineItems = taxCalculation.getLineItems().stream()
.map(item -> {
TaxLineItem lineItem = new TaxLineItem();
lineItem.setTaxTransaction(transaction);
lineItem.setItemReference(item.getReference());
lineItem.setAmount(item.getAmount());
lineItem.setTaxAmount(item.getTaxAmount());
lineItem.setTaxCode(item.getTaxCode());
return lineItem;
})
.collect(Collectors.toList());
transaction.setLineItems(lineItems);
return taxRepository.save(transaction);
}
private Long calculateSubtotal(CalculationLineItemsResponse taxCalculation) {
return taxCalculation.getLineItems().stream()
.mapToLong(LineItemTaxResponse::getAmount)
.sum();
}
private PaymentIntentResponse mapToPaymentIntentResponse(PaymentIntent paymentIntent, 
CalculationLineItemsResponse taxCalculation) {
PaymentIntentResponse response = new PaymentIntentResponse();
response.setId(paymentIntent.getId());
response.setClientSecret(paymentIntent.getClientSecret());
response.setAmount(paymentIntent.getAmount());
response.setCurrency(paymentIntent.getCurrency());
response.setStatus(paymentIntent.getStatus());
response.setTaxCalculation(taxCalculation);
return response;
}
}
2. Invoice with Tax Integration
@Service
@Slf4j
public class InvoiceService {
private final StripeTaxService taxService;
public Invoice createInvoiceWithTax(CreateInvoiceRequest request) throws StripeException {
// Create invoice
Map<String, Object> invoiceParams = new HashMap<>();
invoiceParams.put("customer", request.getCustomerId());
invoiceParams.put("collection_method", "send_invoice");
invoiceParams.put("days_until_due", 30);
Invoice invoice = Invoice.create(invoiceParams);
// Add line items with tax behavior
for (InvoiceLineItemRequest lineItem : request.getLineItems()) {
Map<String, Object> lineItemParams = new HashMap<>();
lineItemParams.put("invoice", invoice.getId());
lineItemParams.put("amount", lineItem.getAmount());
lineItemParams.put("currency", request.getCurrency());
lineItemParams.put("description", lineItem.getDescription());
lineItemParams.put("tax_behavior", "inclusive"); // or "exclusive"
lineItemParams.put("tax_code", lineItem.getTaxCode());
InvoiceItem.create(lineItemParams);
}
// Calculate tax
Map<String, Object> taxParams = new HashMap<>();
taxParams.put("invoice", invoice.getId());
// This will automatically calculate and apply tax
Invoice finalizeInvoice = invoice.finalizeInvoice();
return finalizeInvoice;
}
public Invoice createRecurringSubscriptionWithTax(CreateSubscriptionRequest request) throws StripeException {
// Create product with tax code
Map<String, Object> productParams = new HashMap<>();
productParams.put("name", request.getProductName());
productParams.put("tax_code", request.getTaxCode());
Product product = Product.create(productParams);
// Create price with tax behavior
Map<String, Object> priceParams = new HashMap<>();
priceParams.put("product", product.getId());
priceParams.put("unit_amount", request.getUnitAmount());
priceParams.put("currency", request.getCurrency());
priceParams.put("recurring", Map.of("interval", request.getInterval()));
priceParams.put("tax_behavior", "inclusive");
Price price = Price.create(priceParams);
// Create subscription with default tax rates
Map<String, Object> subscriptionParams = new HashMap<>();
subscriptionParams.put("customer", request.getCustomerId());
subscriptionParams.put("items", List.of(Map.of("price", price.getId())));
subscriptionParams.put("default_tax_rates", request.getTaxRateIds());
Subscription subscription = Subscription.create(subscriptionParams);
return subscription.getLatestInvoiceObject();
}
}

Webhook Handling for Tax Events

1. Webhook Service
@Service
@Slf4j
public class StripeWebhookService {
private final StripeConfig stripeConfig;
private final TaxTransactionRepository taxRepository;
@Async
public void handleWebhookEvent(String payload, String signature) {
try {
Event event = Webhook.constructEvent(payload, signature, stripeConfig.getWebhookSecret());
switch (event.getType()) {
case "tax.calculation.created":
handleTaxCalculationCreated(event);
break;
case "tax.calculation.updated":
handleTaxCalculationUpdated(event);
break;
case "payment_intent.succeeded":
handlePaymentIntentSucceeded(event);
break;
case "payment_intent.payment_failed":
handlePaymentIntentFailed(event);
break;
case "invoice.paid":
handleInvoicePaid(event);
break;
default:
log.debug("Unhandled event type: {}", event.getType());
}
} catch (SignatureVerificationException e) {
log.error("Invalid webhook signature", e);
throw new StripeTaxException("Webhook signature verification failed", e);
} catch (Exception e) {
log.error("Webhook processing failed", e);
throw new StripeTaxException("Webhook processing failed", e);
}
}
private void handleTaxCalculationCreated(Event event) {
TaxCalculation calculation = (TaxCalculation) event.getDataObjectDeserializer().getObject().orElse(null);
if (calculation == null) return;
log.info("Tax calculation created: {}", calculation.getId());
// Store calculation details or update application state
}
private void handlePaymentIntentSucceeded(Event event) {
PaymentIntent paymentIntent = (PaymentIntent) event.getDataObjectDeserializer().getObject().orElse(null);
if (paymentIntent == null) return;
// Update tax transaction status
taxRepository.findById(paymentIntent.getId()).ifPresent(transaction -> {
transaction.setStatus(TaxStatus.APPLIED);
taxRepository.save(transaction);
log.info("Tax applied for payment: {}", paymentIntent.getId());
});
}
private void handleInvoicePaid(Event event) {
Invoice invoice = (Invoice) event.getDataObjectDeserializer().getObject().orElse(null);
if (invoice == null) return;
log.info("Invoice paid with tax: {}", invoice.getId());
// Handle invoice payment with tax
}
}
2. Webhook Controller
@RestController
@RequestMapping("/webhooks/stripe")
@Slf4j
public class StripeWebhookController {
private final StripeWebhookService webhookService;
@PostMapping
public ResponseEntity<String> handleWebhook(@RequestBody String payload,
@RequestHeader("Stripe-Signature") String signature) {
try {
webhookService.handleWebhookEvent(payload, signature);
return ResponseEntity.ok().build();
} catch (StripeTaxException e) {
log.error("Webhook processing error", e);
return ResponseEntity.badRequest().body("Error processing webhook");
}
}
}

Tax Reporting and Compliance

1. Tax Reporting Service
@Service
@Slf4j
public class TaxReportingService {
private final TaxTransactionRepository taxRepository;
public TaxReport generateTaxReport(TaxReportRequest request) {
LocalDateTime startDate = request.getStartDate().atStartOfDay();
LocalDateTime endDate = request.getEndDate().atTime(23, 59, 59);
List<TaxTransaction> transactions = taxRepository.findTransactionsInPeriod(startDate, endDate);
List<TaxSummary> countrySummaries = taxRepository.getTaxSummaryByCountry(startDate, endDate);
TaxReport report = new TaxReport();
report.setPeriodStart(request.getStartDate());
report.setPeriodEnd(request.getEndDate());
report.setTotalTaxAmount(calculateTotalTax(transactions));
report.setTransactionCount(transactions.size());
report.setCountrySummaries(countrySummaries);
report.setTransactions(transactions);
return report;
}
public ReconciliationReport reconcileTaxTransactions(LocalDate startDate, LocalDate endDate) {
try {
// Fetch transactions from Stripe
Map<String, Object> params = new HashMap<>();
params.put("limit", 100);
params.put("created", Map.of(
"gte", startDate.atStartOfDay().toEpochSecond(ZoneOffset.UTC),
"lte", endDate.atTime(23, 59, 59).toEpochSecond(ZoneOffset.UTC)
));
TaxTransactionCollection transactions = TaxTransaction.list(params);
// Compare with local records
List<ReconciliationItem> discrepancies = findDiscrepancies(transactions, startDate, endDate);
ReconciliationReport report = new ReconciliationReport();
report.setStartDate(startDate);
report.setEndDate(endDate);
report.setDiscrepancies(discrepancies);
report.setStatus(discrepancies.isEmpty() ? ReconciliationStatus.MATCH : ReconciliationStatus.MISMATCH);
return report;
} catch (StripeException e) {
log.error("Tax reconciliation failed", e);
throw new StripeTaxException("Tax reconciliation failed", e);
}
}
private Long calculateTotalTax(List<TaxTransaction> transactions) {
return transactions.stream()
.filter(t -> t.getStatus() == TaxStatus.APPLIED)
.mapToLong(TaxTransaction::getTaxAmount)
.sum();
}
private List<ReconciliationItem> findDiscrepancies(TaxTransactionCollection stripeTransactions,
LocalDate startDate, LocalDate endDate) {
List<ReconciliationItem> discrepancies = new ArrayList<>();
// Implementation to compare Stripe transactions with local records
// This would involve comparing amounts, tax calculations, etc.
return discrepancies;
}
}
2. Tax Registration Monitoring
@Service
@Slf4j
public class TaxRegistrationService {
public List<TaxRegistration> getActiveRegistrations() throws StripeException {
Map<String, Object> params = new HashMap<>();
params.put("limit", 50);
RegistrationCollection registrations = Registration.list(params);
return registrations.getData().stream()
.map(this::mapToTaxRegistration)
.collect(Collectors.toList());
}
public TaxRegistrationStatus checkRegistrationRequirement(String country, String state) throws StripeException {
Map<String, Object> params = new HashMap<>();
params.put("country", country);
if (state != null) {
params.put("state", state);
}
CalculationRegistrationDetails details = CalculationRegistrationDetails.retrieve(params);
TaxRegistrationStatus status = new TaxRegistrationStatus();
status.setCountry(country);
status.setState(state);
status.setRegistrationRequired(details.getRegistrationRequirement().getRequired());
status.setActiveRegistration(details.getRegistrationRequirement().getActive());
return status;
}
private TaxRegistration mapToTaxRegistration(Registration stripeRegistration) {
TaxRegistration registration = new TaxRegistration();
registration.setId(stripeRegistration.getId());
registration.setCountry(stripeRegistration.getCountry());
registration.setState(stripeRegistration.getState());
registration.setActive(stripeRegistration.getActive());
registration.setCreated(LocalDateTime.ofInstant(
Instant.ofEpochSecond(stripeRegistration.getCreated()), 
ZoneOffset.UTC
));
return registration;
}
}

Spring Boot Controllers

1. Tax Calculation Controller
@RestController
@RequestMapping("/api/tax")
@Validated
@Slf4j
public class TaxController {
private final StripeTaxService taxService;
private final PaymentService paymentService;
@PostMapping("/calculate")
public ResponseEntity<CalculationLineItemsResponse> calculateTax(
@Valid @RequestBody CalculateTaxRequest request) {
CalculationLineItemsResponse response = taxService.calculateTax(request);
return ResponseEntity.ok(response);
}
@PostMapping("/payment-intent")
public ResponseEntity<PaymentIntentResponse> createPaymentWithTax(
@Valid @RequestBody CreatePaymentIntentRequest request) {
PaymentIntentResponse response = paymentService.createPaymentIntentWithTax(request);
return ResponseEntity.ok(response);
}
}
2. Tax Reporting Controller
@RestController
@RequestMapping("/api/reports/tax")
@Slf4j
public class TaxReportingController {
private final TaxReportingService reportingService;
@GetMapping
public ResponseEntity<TaxReport> generateTaxReport(
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
TaxReportRequest request = new TaxReportRequest(startDate, endDate);
TaxReport report = reportingService.generateTaxReport(request);
return ResponseEntity.ok(report);
}
@GetMapping("/reconciliation")
public ResponseEntity<ReconciliationReport> reconcileTax(
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
ReconciliationReport report = reportingService.reconcileTaxTransactions(startDate, endDate);
return ResponseEntity.ok(report);
}
}

Testing

1. Unit Tests
@ExtendWith(MockitoExtension.class)
class StripeTaxServiceTest {
@Mock
private StripeConfig stripeConfig;
@InjectMocks
private StripeTaxService taxService;
@Test
void shouldCalculateTaxSuccessfully() throws StripeException {
// Given
CalculateTaxRequest request = createTestTaxRequest();
TaxCalculation mockCalculation = createMockTaxCalculation();
// Mock Stripe static method using Mockito
try (MockedStatic<TaxCalculation> mockedTaxCalculation = mockStatic(TaxCalculation.class)) {
mockedTaxCalculation.when(() -> TaxCalculation.create(any(Map.class)))
.thenReturn(mockCalculation);
// When
CalculationLineItemsResponse response = taxService.calculateTax(request);
// Then
assertThat(response).isNotNull();
assertThat(response.getTaxAmount()).isEqualTo(1800L);
assertThat(response.getLineItems()).hasSize(2);
}
}
private CalculateTaxRequest createTestTaxRequest() {
CalculateTaxRequest request = new CalculateTaxRequest();
request.setCurrency("usd");
request.setCustomerId("cus_test123");
Address address = new Address();
address.setLine1("123 Main St");
address.setCity("San Francisco");
address.setState("CA");
address.setPostalCode("94111");
address.setCountry("US");
request.setCustomerAddress(address);
List<LineItemRequest> lineItems = List.of(
createLineItem("item1", 10000L, "txcd_10000000"),
createLineItem("item2", 5000L, "txcd_10000000")
);
request.setLineItems(lineItems);
return request;
}
private LineItemRequest createLineItem(String reference, Long amount, String taxCode) {
LineItemRequest item = new LineItemRequest();
item.setReference(reference);
item.setAmount(amount);
item.setTaxCode(taxCode);
return item;
}
private TaxCalculation createMockTaxCalculation() {
// Create mock TaxCalculation object
// This would typically use a mocking framework or builder pattern
return null; // Simplified for example
}
}

Best Practices

  1. Tax Code Management:
  • Use standardized tax codes from Stripe's taxonomy
  • Maintain mapping between your products and tax codes
  • Regularly update tax codes for compliance
  1. Evidence Collection:
  • Always collect customer location evidence
  • Store IP addresses and billing addresses
  • Implement address validation
  1. Error Handling:
  • Implement fallback tax calculation for Stripe failures
  • Log all tax calculation events
  • Monitor for tax calculation discrepancies
  1. Compliance:
  • Regularly review tax registration requirements
  • Maintain audit trails for tax transactions
  • Implement proper reporting for tax authorities
@Component
@Slf4j
public class TaxComplianceService {
@Scheduled(cron = "0 0 1 * * ?") // Run daily at 1 AM
public void checkTaxRegistrationRequirements() {
try {
List<TaxRegistration> activeRegistrations = getActiveRegistrations();
// Check for new registration requirements
checkNewRegistrationRequirements(activeRegistrations);
// Validate existing registrations
validateActiveRegistrations(activeRegistrations);
} catch (Exception e) {
log.error("Tax registration compliance check failed", e);
}
}
private void checkNewRegistrationRequirements(List<TaxRegistration> activeRegistrations) {
// Implementation to check if new tax registrations are needed
// based on sales volume in new jurisdictions
}
private void validateActiveRegistrations(List<TaxRegistration> activeRegistrations) {
// Validate that all active registrations are still valid
// and alert if any are expiring soon
}
}

Conclusion

Stripe Tax integration in Java provides:

  • Global Tax Compliance: Automatic calculation for 100+ countries
  • Real-time Calculations: Accurate tax amounts during checkout
  • Comprehensive Reporting: Simplified tax reporting and reconciliation
  • Evidence Collection: Automated proof of location gathering
  • Registration Management: Track tax registration requirements

By implementing the patterns shown above, you can build a robust tax compliance system that automatically handles complex tax scenarios across multiple jurisdictions. This ensures your business remains compliant while providing accurate pricing to customers worldwide.

Leave a Reply

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


Macro Nepal Helper