google-site-verification: google61fe8ba583a51912.html
Wave Apps API Integration in Java: Complete Guide

Wave Apps provides accounting, invoicing, and payment processing for small businesses. This guide covers complete integration with Wave's GraphQL 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>wave-apps-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> <graphql.java.version>20.4</graphql.java.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> <!-- GraphQL Java --> <dependency> <groupId>com.graphql-java</groupId> <artifactId>graphql-java</artifactId> <version>${graphql.java.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)

# Wave Apps Configuration wave: api: base-url: ${WAVE_API_URL:https://gql.waveapps.com/graphql/public} access-token: ${WAVE_ACCESS_TOKEN:} business-id: ${WAVE_BUSINESS_ID:} timeout: 30000 max-retries: 3 webhook-secret: ${WAVE_WEBHOOK_SECRET:} # Cache Configuration cache: wave: customers: ttl: 3600 # 1 hour products: ttl: 3600 # 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.wave: DEBUG

Wave Configuration Class

import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Data @Component @ConfigurationProperties(prefix = "wave.api") public class WaveConfig { private String baseUrl; private String accessToken; private String businessId; private int timeout = 30000; private int maxRetries = 3; private String webhookSecret; }

3. Database Entities

Invoice Entity

import lombok.Data; import javax.persistence.*; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.List; @Entity @Table(name = "invoices") @Data public class Invoice { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "wave_invoice_id", unique = true) private String waveInvoiceId; @Column(name = "invoice_number") private String invoiceNumber; @Column(name = "customer_id") private String customerId; @Column(name = "customer_name") private String customerName; @Column(name = "invoice_date") private LocalDateTime invoiceDate; @Column(name = "due_date") private LocalDateTime dueDate; @Column(name = "status") @Enumerated(EnumType.STRING) private InvoiceStatus status; @Column(name = "sub_total", precision = 15, scale = 2) private BigDecimal subTotal; @Column(name = "total_tax", precision = 15, scale = 2) private BigDecimal totalTax; @Column(name = "total_amount", precision = 15, scale = 2) private BigDecimal totalAmount; @Column(name = "amount_due", precision = 15, scale = 2) private BigDecimal amountDue; @Column(name = "currency") private String currency = "USD"; @Column(name = "memo") private String memo; @Column(name = "footer") private String footer; @Column(name = "payment_terms") private String paymentTerms; @Column(name = "created_at") private LocalDateTime createdAt; @Column(name = "updated_at") private LocalDateTime updatedAt; @OneToMany(mappedBy = "invoice", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private List<InvoiceItem> items; @PrePersist protected void onCreate() { createdAt = LocalDateTime.now(); updatedAt = LocalDateTime.now(); } @PreUpdate protected void onUpdate() { updatedAt = LocalDateTime.now(); } public enum InvoiceStatus { DRAFT, SAVED, SENT, PARTIALLY_PAID, PAID, OVERDUE, VOIDED } }

Customer Entity

import lombok.Data; import javax.persistence.*; import java.time.LocalDateTime; @Entity @Table(name = "customers") @Data public class Customer { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "wave_customer_id", unique = true) private String waveCustomerId; @Column(name = "name") private String name; @Column(name = "email") private String email; @Column(name = "phone") private String phone; @Column(name = "address_line1") private String addressLine1; @Column(name = "address_line2") private String addressLine2; @Column(name = "city") private String city; @Column(name = "province") private String province; @Column(name = "country") private String country; @Column(name = "postal_code") private String postalCode; @Column(name = "currency") private String currency = "USD"; @Column(name = "created_at") private LocalDateTime createdAt; @Column(name = "updated_at") private LocalDateTime updatedAt; @PrePersist protected void onCreate() { createdAt = LocalDateTime.now(); updatedAt = LocalDateTime.now(); } @PreUpdate protected void onUpdate() { updatedAt = LocalDateTime.now(); } }

Product/Service Entity

import lombok.Data; import javax.persistence.*; import java.math.BigDecimal; import java.time.LocalDateTime; @Entity @Table(name = "products") @Data public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "wave_product_id", unique = true) private String waveProductId; @Column(name = "name") private String name; @Column(name = "description") private String description; @Column(name = "unit_price", precision = 15, scale = 2) private BigDecimal unitPrice; @Column(name = "currency") private String currency = "USD"; @Column(name = "income_account_id") private String incomeAccountId; @Column(name = "expense_account_id") private String expenseAccountId; @Column(name = "is_sold") private Boolean isSold = true; @Column(name = "is_bought") private Boolean isBought = false; @Column(name = "created_at") private LocalDateTime createdAt; @Column(name = "updated_at") private LocalDateTime updatedAt; @PrePersist protected void onCreate() { createdAt = LocalDateTime.now(); updatedAt = LocalDateTime.now(); } @PreUpdate protected void onUpdate() { updatedAt = LocalDateTime.now(); } }

4. Data Transfer Objects (DTOs)

Invoice Request DTO

import lombok.Data; import javax.validation.constraints.*; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.List; @Data public class InvoiceRequestDTO { @NotBlank(message = "Customer ID is required") private String customerId; private LocalDateTime invoiceDate; private LocalDateTime dueDate; @NotBlank(message = "Currency is required") private String currency = "USD"; private String memo; private String footer; private String paymentTerms; @NotNull(message = "Items are required") @Size(min = 1, message = "At least one item is required") private List<InvoiceItemDTO> items; @Data public static class InvoiceItemDTO { @NotBlank(message = "Product ID is required") private String productId; private String description; @NotNull(message = "Quantity is required") @DecimalMin(value = "0.01", message = "Quantity must be greater than 0") private BigDecimal quantity; @NotNull(message = "Unit price is required") @DecimalMin(value = "0.00", message = "Unit price must be non-negative") private BigDecimal unitPrice; private BigDecimal discountAmount = BigDecimal.ZERO; private BigDecimal discountRate = BigDecimal.ZERO; @NotBlank(message = "Account ID is required") private String accountId; private List<InvoiceItemTaxDTO> taxes; } @Data public static class InvoiceItemTaxDTO { @NotBlank(message = "Tax ID is required") private String taxId; private BigDecimal amount; private BigDecimal rate; } }

Customer Request DTO

import lombok.Data; import javax.validation.constraints.NotBlank; @Data public class CustomerRequestDTO { @NotBlank(message = "Customer name is required") private String name; private String email; private String phone; private String addressLine1; private String addressLine2; private String city; private String province; private String country = "US"; private String postalCode; private String currency = "USD"; }

GraphQL Query DTO

import lombok.Data; @Data public class GraphQLQueryDTO { private String query; private Object variables; private String operationName; public GraphQLQueryDTO(String query) { this.query = query; } public GraphQLQueryDTO(String query, Object variables) { this.query = query; this.variables = variables; } public GraphQLQueryDTO(String query, Object variables, String operationName) { this.query = query; this.variables = variables; this.operationName = operationName; } }

5. GraphQL Query Templates

GraphQL Query Constants

public class WaveGraphQLQueries { // Customer Queries public static final String GET_CUSTOMERS = """ query GetCustomers($businessId: ID!, $page: Int, $pageSize: Int) { business(id: $businessId) { id customers(page: $page, pageSize: $pageSize) { pageInfo { currentPage totalPages totalCount } edges { node { id name firstName lastName email phone address { addressLine1 addressLine2 city province { code name } country { code name } postalCode } currency { code } createdAt modifiedAt } } } } } """; public static final String GET_CUSTOMER_BY_ID = """ query GetCustomer($businessId: ID!, $customerId: ID!) { business(id: $businessId) { id customer(id: $customerId) { id name firstName lastName email phone address { addressLine1 addressLine2 city province { code name } country { code name } postalCode } currency { code } createdAt modifiedAt } } } """; public static final String CREATE_CUSTOMER = """ mutation CreateCustomer($input: CustomerCreateInput!) { customerCreate(input: $input) { didSucceed inputErrors { code message path } customer { id name email phone address { addressLine1 addressLine2 city province { code } country { code } postalCode } currency { code } } } } """; // Invoice Queries public static final String GET_INVOICES = """ query GetInvoices($businessId: ID!, $page: Int, $pageSize: Int, $status: InvoiceStatus) { business(id: $businessId) { id invoices(page: $page, pageSize: $pageSize, status: $status) { pageInfo { currentPage totalPages totalCount } edges { node { id invoiceNumber createdAt modifiedAt invoiceDate dueDate status customer { id name } currency { code } subTotal { value } totalTax { value } totalAmount { value } amountDue { value } memo footer items { product { id name } description quantity unitPrice subtotal { value } totalTax { value } total { value } } } } } } } """; public static final String GET_INVOICE_BY_ID = """ query GetInvoice($businessId: ID!, $invoiceId: ID!) { business(id: $businessId) { id invoice(id: $invoiceId) { id invoiceNumber createdAt modifiedAt invoiceDate dueDate status customer { id name email phone address { addressLine1 addressLine2 city province { code name } country { code name } postalCode } } currency { code } subTotal { value } totalTax { value } totalAmount { value } amountDue { value } memo footer items { product { id name } description quantity unitPrice subtotal { value } totalTax { value } total { value } taxes { amount { value } salesTax { id name rate } } } } } } """; public static final String CREATE_INVOICE = """ mutation CreateInvoice($input: InvoiceCreateInput!) { invoiceCreate(input: $input) { didSucceed inputErrors { code message path } invoice { id invoiceNumber invoiceDate dueDate status customer { id name } subTotal { value } totalTax { value } totalAmount { value } amountDue { value } items { product { id name } description quantity unitPrice subtotal { value } totalTax { value } total { value } } } } } """; public static final String SEND_INVOICE = """ mutation SendInvoice($input: InvoiceSendInput!) { invoiceSend(input: $input) { didSucceed inputErrors { code message path } invoice { id status lastSentAt lastSentVia } } } """; // Product Queries public static final String GET_PRODUCTS = """ query GetProducts($businessId: ID!, $page: Int, $pageSize: Int) { business(id: $businessId) { id products(page: $page, pageSize: $pageSize, isSold: true) { pageInfo { currentPage totalPages totalCount } edges { node { id name description unitPrice currency { code } incomeAccount { id name } expenseAccount { id name } isSold isBought createdAt modifiedAt } } } } } """; public static final String CREATE_PRODUCT = """ mutation CreateProduct($input: ProductCreateInput!) { productCreate(input: $input) { didSucceed inputErrors { code message path } product { id name description unitPrice currency { code } incomeAccount { id name } expenseAccount { id name } isSold isBought } } } """; // Business Info Query public static final String GET_BUSINESS_INFO = """ query GetBusiness($businessId: ID!) { business(id: $businessId) { id name isPersonal currency { code } createdAt modifiedAt addresses { addressLine1 addressLine2 city province { code name } country { code name } postalCode } } } """; }

6. Wave API Client Service

Main Wave 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.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; @Slf4j @Service public class WaveApiClient { private final WaveConfig config; private final OkHttpClient httpClient; private final ObjectMapper objectMapper; @Autowired public WaveApiClient(WaveConfig 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.getAccessToken())) .addInterceptor(new RetryInterceptor(config.getMaxRetries(), 1000)) .build(); } /** * Execute GraphQL query */ public Map<String, Object> executeQuery(String query, Map<String, Object> variables) throws WaveApiException { try { GraphQLQueryDTO graphQLQuery = new GraphQLQueryDTO(query, variables); String jsonBody = objectMapper.writeValueAsString(graphQLQuery); RequestBody body = RequestBody.create( jsonBody, MediaType.parse("application/json") ); Request request = new Request.Builder() .url(config.getBaseUrl()) .post(body) .build(); String responseBody = executeRequest(request); Map<String, Object> response = objectMapper.readValue(responseBody, new TypeReference<Map<String, Object>>() {}); // Check for GraphQL errors if (response.containsKey("errors")) { log.error("GraphQL errors: {}", response.get("errors")); throw new WaveApiException("GraphQL query failed: " + response.get("errors")); } return response; } catch (IOException e) { log.error("Error executing GraphQL query: {}", e.getMessage(), e); throw new WaveApiException("Failed to execute GraphQL query", e); } } /** * Get business information */ public Map<String, Object> getBusinessInfo() throws WaveApiException { Map<String, Object> variables = Map.of("businessId", config.getBusinessId()); return executeQuery(WaveGraphQLQueries.GET_BUSINESS_INFO, variables); } /** * Get customers with pagination */ @Cacheable(value = "waveCustomers", key = "{#page, #pageSize}") public Map<String, Object> getCustomers(Integer page, Integer pageSize) throws WaveApiException { Map<String, Object> variables = new HashMap<>(); variables.put("businessId", config.getBusinessId()); if (page != null) variables.put("page", page); if (pageSize != null) variables.put("pageSize", pageSize); return executeQuery(WaveGraphQLQueries.GET_CUSTOMERS, variables); } /** * Get customer by ID */ @Cacheable(value = "waveCustomer", key = "#customerId") public Map<String, Object> getCustomerById(String customerId) throws WaveApiException { Map<String, Object> variables = Map.of( "businessId", config.getBusinessId(), "customerId", customerId ); return executeQuery(WaveGraphQLQueries.GET_CUSTOMER_BY_ID, variables); } /** * Create customer */ public Map<String, Object> createCustomer(Map<String, Object> customerInput) throws WaveApiException { Map<String, Object> variables = Map.of("input", customerInput); return executeQuery(WaveGraphQLQueries.CREATE_CUSTOMER, variables); } /** * Get invoices with pagination and filters */ public Map<String, Object> getInvoices(Integer page, Integer pageSize, String status) throws WaveApiException { Map<String, Object> variables = new HashMap<>(); variables.put("businessId", config.getBusinessId()); if (page != null) variables.put("page", page); if (pageSize != null) variables.put("pageSize", pageSize); if (status != null) variables.put("status", status); return executeQuery(WaveGraphQLQueries.GET_INVOICES, variables); } /** * Get invoice by ID */ public Map<String, Object> getInvoiceById(String invoiceId) throws WaveApiException { Map<String, Object> variables = Map.of( "businessId", config.getBusinessId(), "invoiceId", invoiceId ); return executeQuery(WaveGraphQLQueries.GET_INVOICE_BY_ID, variables); } /** * Create invoice */ public Map<String, Object> createInvoice(Map<String, Object> invoiceInput) throws WaveApiException { Map<String, Object> variables = Map.of("input", invoiceInput); return executeQuery(WaveGraphQLQueries.CREATE_INVOICE, variables); } /** * Send invoice */ public Map<String, Object> sendInvoice(String invoiceId, String deliveryMethod) throws WaveApiException { Map<String, Object> input = Map.of( "invoiceId", invoiceId, "deliveryMethod", deliveryMethod != null ? deliveryMethod : "EMAIL" ); Map<String, Object> variables = Map.of("input", input); return executeQuery(WaveGraphQLQueries.SEND_INVOICE, variables); } /** * Get products with pagination */ @Cacheable(value = "waveProducts", key = "{#page, #pageSize}") public Map<String, Object> getProducts(Integer page, Integer pageSize) throws WaveApiException { Map<String, Object> variables = new HashMap<>(); variables.put("businessId", config.getBusinessId()); if (page != null) variables.put("page", page); if (pageSize != null) variables.put("pageSize", pageSize); return executeQuery(WaveGraphQLQueries.GET_PRODUCTS, variables); } /** * Create product */ public Map<String, Object> createProduct(Map<String, Object> productInput) throws WaveApiException { Map<String, Object> variables = Map.of("input", productInput); return executeQuery(WaveGraphQLQueries.CREATE_PRODUCT, variables); } /** * Delete invoice (Void) */ public Map<String, Object> deleteInvoice(String invoiceId) throws WaveApiException { String deleteInvoiceMutation = """ mutation DeleteInvoice($input: InvoiceDeleteInput!) { invoiceDelete(input: $input) { didSucceed inputErrors { code message path } } } """; Map<String, Object> input = Map.of("invoiceId", invoiceId); Map<String, Object> variables = Map.of("input", input); return executeQuery(deleteInvoiceMutation, variables); } private String executeRequest(Request request) throws IOException, WaveApiException { try (Response response = httpClient.newCall(request).execute()) { if (!response.isSuccessful()) { String errorBody = response.body().string(); log.error("Wave API error: {} - {}", response.code(), errorBody); throw new WaveApiException("Wave API error: " + response.code() + " - " + errorBody); } return response.body().string(); } } }

Authentication Interceptor

import okhttp3.*; import org.jetbrains.annotations.NotNull; import java.io.IOException; public class AuthInterceptor implements Interceptor { private final String accessToken; public AuthInterceptor(String accessToken) { this.accessToken = accessToken; } @NotNull @Override public Response intercept(@NotNull Chain chain) throws IOException { Request originalRequest = chain.request(); Request authenticatedRequest = originalRequest.newBuilder() .header("Authorization", "Bearer " + accessToken) .header("Content-Type", "application/json") .build(); return chain.proceed(authenticatedRequest); } }

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("Wave API request failed with status {} on attempt {}/{}", response.code(), attempt, maxRetries); } catch (IOException e) { exception = e; logger.warn("Wave 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; } }

7. Wave Processing Service

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.time.format.DateTimeFormatter; import java.util.*; @Slf4j @Service public class WaveProcessingService { @Autowired private WaveApiClient waveApiClient; @Autowired private InvoiceRepository invoiceRepository; @Autowired private CustomerRepository customerRepository; @Autowired private ProductRepository productRepository; /** * Create customer in Wave */ @Transactional public Map<String, Object> createCustomer(CustomerRequestDTO customerRequest) throws WaveApiException { try { Map<String, Object> customerInput = new HashMap<>(); customerInput.put("businessId", waveApiClient.getConfig().getBusinessId()); customerInput.put("name", customerRequest.getName()); customerInput.put("email", customerRequest.getEmail()); customerInput.put("phone", customerRequest.getPhone()); customerInput.put("currency", customerRequest.getCurrency()); // Address if (customerRequest.getAddressLine1() != null) { Map<String, Object> address = new HashMap<>(); address.put("addressLine1", customerRequest.getAddressLine1()); address.put("addressLine2", customerRequest.getAddressLine2()); address.put("city", customerRequest.getCity()); address.put("provinceCode", customerRequest.getProvince()); address.put("countryCode", customerRequest.getCountry()); address.put("postalCode", customerRequest.getPostalCode()); customerInput.put("address", address); } Map<String, Object> response = waveApiClient.createCustomer(customerInput); // Save customer to local database saveCustomerToDatabase(response, customerRequest); log.info("Customer created successfully: {}", customerRequest.getName()); return response; } catch (Exception e) { log.error("Error creating customer: {}", e.getMessage(), e); throw new WaveApiException("Customer creation failed: " + e.getMessage(), e); } } /** * Create invoice in Wave */ @Transactional public Map<String, Object> createInvoice(InvoiceRequestDTO invoiceRequest) throws WaveApiException { try { Map<String, Object> invoiceInput = prepareInvoiceInput(invoiceRequest); Map<String, Object> response = waveApiClient.createInvoice(invoiceInput); // Save invoice to local database saveInvoiceToDatabase(response, invoiceRequest); log.info("Invoice created successfully for customer: {}", invoiceRequest.getCustomerId()); return response; } catch (Exception e) { log.error("Error creating invoice: {}", e.getMessage(), e); throw new WaveApiException("Invoice creation failed: " + e.getMessage(), e); } } /** * Send invoice via email */ public Map<String, Object> sendInvoice(String invoiceId, String deliveryMethod) throws WaveApiException { try { Map<String, Object> response = waveApiClient.sendInvoice(invoiceId, deliveryMethod); // Update invoice status in local database invoiceRepository.findByWaveInvoiceId(invoiceId).ifPresent(invoice -> { invoice.setStatus(Invoice.InvoiceStatus.SENT); invoiceRepository.save(invoice); }); log.info("Invoice sent successfully: {}", invoiceId); return response; } catch (Exception e) { log.error("Error sending invoice: {}", e.getMessage(), e); throw new WaveApiException("Invoice sending failed: " + e.getMessage(), e); } } /** * Get customer by ID */ public Map<String, Object> getCustomer(String customerId) throws WaveApiException { try { return waveApiClient.getCustomerById(customerId); } catch (Exception e) { log.error("Error getting customer: {}", e.getMessage(), e); throw new WaveApiException("Failed to get customer: " + e.getMessage(), e); } } /** * Get all customers with pagination */ public Map<String, Object> getCustomers(Integer page, Integer pageSize) throws WaveApiException { try { return waveApiClient.getCustomers(page, pageSize); } catch (Exception e) { log.error("Error getting customers: {}", e.getMessage(), e); throw new WaveApiException("Failed to get customers: " + e.getMessage(), e); } } /** * Get all products with pagination */ public Map<String, Object> getProducts(Integer page, Integer pageSize) throws WaveApiException { try { return waveApiClient.getProducts(page, pageSize); } catch (Exception e) { log.error("Error getting products: {}", e.getMessage(), e); throw new WaveApiException("Failed to get products: " + e.getMessage(), e); } } /** * Get business information */ public Map<String, Object> getBusinessInfo() throws WaveApiException { try { return waveApiClient.getBusinessInfo(); } catch (Exception e) { log.error("Error getting business info: {}", e.getMessage(), e); throw new WaveApiException("Failed to get business info: " + e.getMessage(), e); } } /** * Prepare invoice input for Wave API */ @SuppressWarnings("unchecked") private Map<String, Object> prepareInvoiceInput(InvoiceRequestDTO invoiceRequest) { Map<String, Object> invoiceInput = new HashMap<>(); invoiceInput.put("businessId", waveApiClient.getConfig().getBusinessId()); invoiceInput.put("customerId", invoiceRequest.getCustomerId()); invoiceInput.put("currency", invoiceRequest.getCurrency()); // Dates DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE; if (invoiceRequest.getInvoiceDate() != null) { invoiceInput.put("invoiceDate", invoiceRequest.getInvoiceDate().format(formatter)); } else { invoiceInput.put("invoiceDate", LocalDateTime.now().format(formatter)); } if (invoiceRequest.getDueDate() != null) { invoiceInput.put("dueDate", invoiceRequest.getDueDate().format(formatter)); } // Optional fields if (invoiceRequest.getMemo() != null) { invoiceInput.put("memo", invoiceRequest.getMemo()); } if (invoiceRequest.getFooter() != null) { invoiceInput.put("footer", invoiceRequest.getFooter()); } if (invoiceRequest.getPaymentTerms() != null) { invoiceInput.put("paymentTerms", invoiceRequest.getPaymentTerms()); } // Items List<Map<String, Object>> items = new ArrayList<>(); for (InvoiceRequestDTO.InvoiceItemDTO item : invoiceRequest.getItems()) { Map<String, Object> itemInput = new HashMap<>(); itemInput.put("productId", item.getProductId()); itemInput.put("description", item.getDescription()); itemInput.put("quantity", item.getQuantity()); itemInput.put("unitPrice", item.getUnitPrice()); itemInput.put("accountId", item.getAccountId()); if (item.getDiscountAmount().compareTo(BigDecimal.ZERO) > 0) { itemInput.put("discountAmount", item.getDiscountAmount()); } else if (item.getDiscountRate().compareTo(BigDecimal.ZERO) > 0) { itemInput.put("discountRate", item.getDiscountRate()); } // Taxes if (item.getTaxes() != null && !item.getTaxes().isEmpty()) { List<Map<String, Object>> taxes = new ArrayList<>(); for (InvoiceRequestDTO.InvoiceItemTaxDTO tax : item.getTaxes()) { Map<String, Object> taxInput = new HashMap<>(); taxInput.put("salesTaxId", tax.getTaxId()); if (tax.getAmount() != null) { taxInput.put("amount", tax.getAmount()); } if (tax.getRate() != null) { taxInput.put("rate", tax.getRate()); } taxes.add(taxInput); } itemInput.put("taxes", taxes); } items.add(itemInput); } invoiceInput.put("items", items); return invoiceInput; } /** * Save customer to local database */ @SuppressWarnings("unchecked") private void saveCustomerToDatabase(Map<String, Object> response, CustomerRequestDTO customerRequest) { try { Map<String, Object> data = (Map<String, Object>) response.get("data"); if (data != null) { Map<String, Object> customerCreate = (Map<String, Object>) data.get("customerCreate"); if (customerCreate != null && Boolean.TRUE.equals(customerCreate.get("didSucceed"))) { Map<String, Object> customerData = (Map<String, Object>) customerCreate.get("customer"); Customer customer = new Customer(); customer.setWaveCustomerId((String) customerData.get("id")); customer.setName(customerRequest.getName()); customer.setEmail(customerRequest.getEmail()); customer.setPhone(customerRequest.getPhone()); customer.setAddressLine1(customerRequest.getAddressLine1()); customer.setAddressLine2(customerRequest.getAddressLine2()); customer.setCity(customerRequest.getCity()); customer.setProvince(customerRequest.getProvince()); customer.setCountry(customerRequest.getCountry()); customer.setPostalCode(customerRequest.getPostalCode()); customer.setCurrency(customerRequest.getCurrency()); customerRepository.save(customer); log.info("Customer saved to local database: {}", customer.getWaveCustomerId()); } } } catch (Exception e) { log.error("Error saving customer to local database: {}", e.getMessage(), e); } } /** * Save invoice to local database */ @SuppressWarnings("unchecked") private void saveInvoiceToDatabase(Map<String, Object> response, InvoiceRequestDTO invoiceRequest) { try { Map<String, Object> data = (Map<String, Object>) response.get("data"); if (data != null) { Map<String, Object> invoiceCreate = (Map<String, Object>) data.get("invoiceCreate"); if (invoiceCreate != null && Boolean.TRUE.equals(invoiceCreate.get("didSucceed"))) { Map<String, Object> invoiceData = (Map<String, Object>) invoiceCreate.get("invoice"); Invoice invoice = new Invoice(); invoice.setWaveInvoiceId((String) invoiceData.get("id")); invoice.setCustomerId(invoiceRequest.getCustomerId()); invoice.setInvoiceDate(invoiceRequest.getInvoiceDate() != null ? invoiceRequest.getInvoiceDate() : LocalDateTime.now()); invoice.setDueDate(invoiceRequest.getDueDate()); invoice.setCurrency(invoiceRequest.getCurrency()); invoice.setMemo(invoiceRequest.getMemo()); invoice.setFooter(invoiceRequest.getFooter()); invoice.setPaymentTerms(invoiceRequest.getPaymentTerms()); invoice.setStatus(Invoice.InvoiceStatus.DRAFT); // Set amounts from response Map<String, Object> subTotal = (Map<String, Object>) invoiceData.get("subTotal"); Map<String, Object> totalTax = (Map<String, Object>) invoiceData.get("totalTax"); Map<String, Object> totalAmount = (Map<String, Object>) invoiceData.get("totalAmount"); Map<String, Object> amountDue = (Map<String, Object>) invoiceData.get("amountDue"); if (subTotal != null) { invoice.setSubTotal(new BigDecimal(subTotal.get("value").toString())); } if (totalTax != null) { invoice.setTotalTax(new BigDecimal(totalTax.get("value").toString())); } if (totalAmount != null) { invoice.setTotalAmount(new BigDecimal(totalAmount.get("value").toString())); } if (amountDue != null) { invoice.setAmountDue(new BigDecimal(amountDue.get("value").toString())); } invoiceRepository.save(invoice); log.info("Invoice saved to local database: {}", invoice.getWaveInvoiceId()); } } } catch (Exception e) { log.error("Error saving invoice to local database: {}", e.getMessage(), e); } } }

8. REST Controllers

Wave 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/wave") public class WaveController { @Autowired private WaveProcessingService waveProcessingService; @Autowired private WaveApiClient waveApiClient; /** * Get business information */ @GetMapping("/business") public ResponseEntity<?> getBusinessInfo() { try { Map<String, Object> response = waveProcessingService.getBusinessInfo(); return ResponseEntity.ok(response); } catch (WaveApiException e) { log.error("Error getting business info: {}", e.getMessage(), e); return createErrorResponse("Failed to get business information", e.getMessage()); } } /** * Create customer */ @PostMapping("/customers") public ResponseEntity<?> createCustomer(@Valid @RequestBody CustomerRequestDTO customerRequest) { try { Map<String, Object> response = waveProcessingService.createCustomer(customerRequest); return ResponseEntity.ok(response); } catch (WaveApiException e) { log.error("Error creating customer: {}", e.getMessage(), e); return createErrorResponse("Failed to create customer", e.getMessage()); } } /** * Get customers */ @GetMapping("/customers") public ResponseEntity<?> getCustomers( @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "50") Integer pageSize) { try { Map<String, Object> response = waveProcessingService.getCustomers(page, pageSize); return ResponseEntity.ok(response); } catch (WaveApiException e) { log.error("Error getting customers: {}", e.getMessage(), e); return createErrorResponse("Failed to get customers", e.getMessage()); } } /** * Get customer by ID */ @GetMapping("/customers/{customerId}") public ResponseEntity<?> getCustomer(@PathVariable String customerId) { try { Map<String, Object> response = waveProcessingService.getCustomer(customerId); return ResponseEntity.ok(response); } catch (WaveApiException e) { log.error("Error getting customer: {}", e.getMessage(), e); return createErrorResponse("Failed to get customer", e.getMessage()); } } /** * Create invoice */ @PostMapping("/invoices") public ResponseEntity<?> createInvoice(@Valid @RequestBody InvoiceRequestDTO invoiceRequest) { try { Map<String, Object> response = waveProcessingService.createInvoice(invoiceRequest); return ResponseEntity.ok(response); } catch (WaveApiException e) { log.error("Error creating invoice: {}", e.getMessage(), e); return createErrorResponse("Failed to create invoice", e.getMessage()); } } /** * Send invoice */ @PostMapping("/invoices/{invoiceId}/send") public ResponseEntity<?> sendInvoice( @PathVariable String invoiceId, @RequestParam(defaultValue = "EMAIL") String deliveryMethod) { try { Map<String, Object> response = waveProcessingService.sendInvoice(invoiceId, deliveryMethod); return ResponseEntity.ok(response); } catch (WaveApiException e) { log.error("Error sending invoice: {}", e.getMessage(), e); return createErrorResponse("Failed to send invoice", e.getMessage()); } } /** * Get invoices */ @GetMapping("/invoices") public ResponseEntity<?> getInvoices( @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "50") Integer pageSize, @RequestParam(required = false) String status) { try { Map<String, Object> response = waveApiClient.getInvoices(page, pageSize, status); return ResponseEntity.ok(response); } catch (WaveApiException e) { log.error("Error getting invoices: {}", e.getMessage(), e); return createErrorResponse("Failed to get invoices", e.getMessage()); } } /** * Get products */ @GetMapping("/products") public ResponseEntity<?> getProducts( @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "50") Integer pageSize) { try { Map<String, Object> response = waveProcessingService.getProducts(page, pageSize); return ResponseEntity.ok(response); } catch (WaveApiException e) { log.error("Error getting products: {}", e.getMessage(), e); return createErrorResponse("Failed to get products", e.getMessage()); } } /** * Delete invoice */ @DeleteMapping("/invoices/{invoiceId}") public ResponseEntity<?> deleteInvoice(@PathVariable String invoiceId) { try { Map<String, Object> response = waveApiClient.deleteInvoice(invoiceId); return ResponseEntity.ok(response); } catch (WaveApiException e) { log.error("Error deleting invoice: {}", e.getMessage(), e); return createErrorResponse("Failed to delete invoice", e.getMessage()); } } private ResponseEntity<Map<String, Object>> createErrorResponse(String message, String errorDetail) { Map<String, Object> errorResponse = new HashMap<>(); errorResponse.put("error", true); errorResponse.put("message", message); errorResponse.put("error_detail", errorDetail); return ResponseEntity.badRequest().body(errorResponse); } }

9. Custom Exception

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

10. 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.time.LocalDateTime; import java.util.List; import java.util.Optional; @Repository public interface InvoiceRepository extends JpaRepository<Invoice, Long> { Optional<Invoice> findByWaveInvoiceId(String waveInvoiceId); List<Invoice> findByCustomerId(String customerId); List<Invoice> findByStatus(Invoice.InvoiceStatus status); List<Invoice> findByInvoiceDateBetween(LocalDateTime start, LocalDateTime end); @Query("SELECT SUM(i.totalAmount) FROM Invoice i WHERE i.status = 'PAID' AND i.invoiceDate BETWEEN :start AND :end") Double getTotalRevenueByPeriod(@Param("start") LocalDateTime start, @Param("end") LocalDateTime end); } @Repository public interface CustomerRepository extends JpaRepository<Customer, Long> { Optional<Customer> findByWaveCustomerId(String waveCustomerId); Optional<Customer> findByEmail(String email); List<Customer> findByNameContainingIgnoreCase(String name); List<Customer> findByCountry(String country); } @Repository public interface ProductRepository extends JpaRepository<Product, Long> { Optional<Product> findByWaveProductId(String waveProductId); List<Product> findByNameContainingIgnoreCase(String name); List<Product> findByIsSoldTrue(); }

This comprehensive Wave Apps API integration provides complete functionality for managing customers, products, invoices, and business operations through Wave's GraphQL API in your Java application.

Leave a Reply

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


Macro Nepal Helper