google-site-verification: google61fe8ba583a51912.html
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