Adding New Integrations
Overview
This comprehensive guide walks you through the process of adding new third-party integrations to the Ink platform, including client interface design, configuration management, authentication handling, data mapping, testing, and documentation.
Target Audience: Backend developers and integration engineers
Prerequisites: Understanding of Integration Patterns
Estimated Time: 45-60 minutes
Prerequisites
- Completed Integration Patterns
- Understanding of REST APIs and OAuth 2.0
- Knowledge of Spring Boot configuration
- Familiarity with the target integration API
Integration Development Process
Installation Steps
1. Create Integration Module Structure
# Create directory structure for new integration
mkdir -p src/main/java/com/jetrev/ink/integration/{name}
mkdir -p src/main/java/com/jetrev/ink/integration/{name}/client
mkdir -p src/main/java/com/jetrev/ink/integration/{name}/model
mkdir -p src/main/java/com/jetrev/ink/integration/{name}/config
mkdir -p src/main/java/com/jetrev/ink/integration/{name}/service
mkdir -p src/test/java/com/jetrev/ink/integration/{name}
2. Add Integration Dependencies
<!-- filepath: /Users/jetstart/dev/jetrev/ink/pom.xml -->
<!-- ...existing code... -->
<dependencies>
<!-- Add integration-specific dependencies -->
<dependency>
<groupId>com.example</groupId>
<artifactId>example-api-client</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
<!-- ...existing code... -->
3. Configuration Properties
# filepath: /Users/jetstart/dev/jetrev/ink/src/main/resources/application.yml
# ...existing code...
integration:
example:
enabled: ${EXAMPLE_INTEGRATION_ENABLED:false}
baseUrl: ${EXAMPLE_BASE_URL:https://api.example.com}
apiKey: ${EXAMPLE_API_KEY}
apiSecret: ${EXAMPLE_API_SECRET}
timeout: 30000
connectionPoolSize: 20
retryMaxAttempts: 3
retryBackoffInterval: 1000
circuitBreakerEnabled: true
rateLimitPerSecond: 10
# ...existing code...
Configuration
Step 1: Define Configuration Properties Class
// filepath: /Users/jetstart/dev/jetrev/ink/src/main/java/com/jetrev/ink/integration/example/config/ExampleIntegrationProperties.java
@Configuration
@ConfigurationProperties(prefix = "integration.example")
@Data
@Validated
public class ExampleIntegrationProperties {
private boolean enabled = false;
@NotBlank(message = "Base URL is required")
private String baseUrl;
@NotBlank(message = "API key is required")
private String apiKey;
private String apiSecret;
private int timeout = 30000;
private int connectionPoolSize = 20;
private int retryMaxAttempts = 3;
private long retryBackoffInterval = 1000;
private boolean circuitBreakerEnabled = true;
private int rateLimitPerSecond = 10;
}
Step 2: Create Data Models
// filepath: /Users/jetstart/dev/jetrev/ink/src/main/java/com/jetrev/ink/integration/example/model/ExampleCustomer.java
// Customer Models (existing)
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ExampleCustomer {
private String id;
private String email;
private String name;
private String phone;
private ExampleAddress address;
private Instant createdAt;
private Map<String, Object> metadata;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ExampleAddress {
private String line1;
private String line2;
private String city;
private String state;
private String postalCode;
private String country;
}
// Request DTOs
@Data
@Builder
public class CreateExampleCustomerRequest {
@NotBlank
private String email;
@NotBlank
private String name;
private String phone;
private ExampleAddress address;
private Map<String, Object> metadata;
}
// Response DTOs
@Data
public class ExampleApiResponse<T> {
private boolean success;
private T data;
private String message;
private List<ExampleError> errors;
}
@Data
public class ExampleError {
private String code;
private String message;
private String field;
}
// Project Models
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ExampleProject {
private String id;
private String name;
private String description;
private String customerId;
private String status; // ACTIVE, INACTIVE, COMPLETED
private Instant startDate;
private Instant endDate;
private Map<String, Object> metadata;
private Instant createdAt;
private Instant updatedAt;
}
@Data
@Builder
public class CreateExampleProjectRequest {
@NotBlank
private String name;
private String description;
@NotBlank
private String customerId;
private String status;
private Instant startDate;
private Instant endDate;
private Map<String, Object> metadata;
}
@Data
@Builder
public class UpdateExampleProjectRequest {
private String name;
private String description;
private String status;
private Instant startDate;
private Instant endDate;
private Map<String, Object> metadata;
}
// Agreement (MSA) Models
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ExampleAgreement {
private String id;
private String agreementNumber;
private String customerId;
private String projectId;
private String status; // DRAFT, ACTIVE, EXPIRED, TERMINATED
private Instant effectiveDate;
private Instant expirationDate;
private BigDecimal totalValue;
private String terms;
private List<ExampleAgreementItem> items;
private Instant createdAt;
private Instant updatedAt;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ExampleAgreementItem {
private String id;
private String productId;
private String description;
private Integer quantity;
private BigDecimal unitPrice;
private BigDecimal total;
}
@Data
@Builder
public class CreateExampleAgreementRequest {
@NotBlank
private String customerId;
@NotBlank
private String projectId;
@NotNull
private Instant effectiveDate;
@NotNull
private Instant expirationDate;
private String terms;
private List<ExampleAgreementItem> items;
}
// Subscription Models
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ExampleSubscription {
private String id;
private String customerId;
private String planId;
private String status; // ACTIVE, CANCELED, PAST_DUE, TRIALING
private Instant currentPeriodStart;
private Instant currentPeriodEnd;
private Instant trialStart;
private Instant trialEnd;
private Boolean cancelAtPeriodEnd;
private BigDecimal amount;
private String currency;
private String billingInterval; // MONTHLY, YEARLY
private Instant createdAt;
private Instant updatedAt;
}
@Data
@Builder
public class CreateExampleSubscriptionRequest {
@NotBlank
private String customerId;
@NotBlank
private String planId;
private Integer trialDays;
private Map<String, Object> metadata;
}
@Data
@Builder
public class UpdateExampleSubscriptionRequest {
private String planId;
private Boolean cancelAtPeriodEnd;
private Map<String, Object> metadata;
}
// Product Models
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ExampleProduct {
private String id;
private String sku;
private String name;
private String description;
private BigDecimal price;
private String currency;
private String productType; // LICENSE, SUBSCRIPTION, SERVICE
private Boolean active;
private Map<String, Object> metadata;
private Instant createdAt;
private Instant updatedAt;
}
@Data
@Builder
public class CreateExampleProductRequest {
@NotBlank
private String sku;
@NotBlank
private String name;
private String description;
@NotNull
private BigDecimal price;
private String currency;
@NotBlank
private String productType;
private Boolean active;
private Map<String, Object> metadata;
}
// License Models
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ExampleLicense {
private String id;
private String licenseKey;
private String customerId;
private String productId;
private String licenseType; // TRIAL, PERPETUAL, SUBSCRIPTION
private String status; // ACTIVE, EXPIRED, REVOKED, SUSPENDED
private Integer maxActivations;
private Integer currentActivations;
private Instant expiresAt;
private Instant issuedAt;
private Instant activatedAt;
private Map<String, Object> metadata;
}
@Data
@Builder
public class CreateExampleLicenseRequest {
@NotBlank
private String customerId;
@NotBlank
private String productId;
@NotBlank
private String licenseType;
private Integer maxActivations;
private Instant expiresAt;
private Map<String, Object> metadata;
}
@Data
@Builder
public class UpdateExampleLicenseRequest {
private String status;
private Integer maxActivations;
private Instant expiresAt;
}
// Quote Models
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ExampleQuote {
private String id;
private String quoteNumber;
private String customerId;
private String projectId;
private String status; // DRAFT, SENT, ACCEPTED, REJECTED, EXPIRED
private BigDecimal subtotal;
private BigDecimal tax;
private BigDecimal discount;
private BigDecimal total;
private String currency;
private Instant validUntil;
private List<ExampleQuoteItem> items;
private Instant createdAt;
private Instant updatedAt;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ExampleQuoteItem {
private String id;
private String productId;
private String description;
private Integer quantity;
private BigDecimal unitPrice;
private BigDecimal discount;
private BigDecimal total;
}
@Data
@Builder
public class CreateExampleQuoteRequest {
@NotBlank
private String customerId;
private String projectId;
@NotNull
private Instant validUntil;
@NotEmpty
private List<ExampleQuoteItem> items;
private BigDecimal discount;
}
// Order Models
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ExampleOrder {
private String id;
private String orderNumber;
private String customerId;
private String status; // PENDING, PAID, FULFILLED, CANCELED, REFUNDED
private BigDecimal subtotal;
private BigDecimal tax;
private BigDecimal shipping;
private BigDecimal discount;
private BigDecimal total;
private String currency;
private String paymentMethod;
private String paymentId;
private Instant paidAt;
private Instant fulfilledAt;
private String trackingNumber;
private List<ExampleOrderItem> items;
private Instant createdAt;
private Instant updatedAt;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ExampleOrderItem {
private String id;
private String productId;
private Integer quantity;
private BigDecimal unitPrice;
private BigDecimal total;
}
@Data
@Builder
public class CreateExampleOrderRequest {
@NotBlank
private String customerId;
@NotEmpty
private List<ExampleOrderItem> items;
private BigDecimal shipping;
private BigDecimal discount;
}
@Data
@Builder
public class UpdateExampleOrderRequest {
private String status;
private String trackingNumber;
private String paymentId;
}
Step 4: Create Integration Client
// filepath: /Users/jetstart/dev/jetrev/ink/src/main/java/com/jetrev/ink/integration/example/client/ExampleClient.java
@Service
@Slf4j
@ConditionalOnProperty(name = "integration.example.enabled", havingValue = "true")
public class ExampleClient extends BaseIntegrationClient {
private final ExampleIntegrationProperties properties;
private final ExampleAuthenticationProvider authProvider;
private final CircuitBreaker circuitBreaker;
private final RateLimiter rateLimiter;
public ExampleClient(
@Qualifier("exampleWebClient") WebClient webClient,
ObjectMapper objectMapper,
ExampleIntegrationProperties properties,
ExampleAuthenticationProvider authProvider,
CircuitBreakerRegistry circuitBreakerRegistry,
RateLimiterRegistry rateLimiterRegistry) {
super(webClient, objectMapper);
this.properties = properties;
this.authProvider = authProvider;
this.circuitBreaker = circuitBreakerRegistry.circuitBreaker("example");
this.rateLimiter = rateLimiterRegistry.rateLimiter("example");
}
// ===== CUSTOMER OPERATIONS (existing) =====
public ExampleCustomer createCustomer(CreateExampleCustomerRequest request) {
log.info("Creating customer in Example API: {}", request.getEmail());
return executeWithResilience(() -> {
return post("/v1/customers", request, ExampleCustomer.class)
.block();
});
}
public ExampleCustomer getCustomer(String customerId) {
log.info("Fetching customer from Example API: {}", customerId);
return executeWithResilience(() -> {
return get("/v1/customers/" + customerId, ExampleCustomer.class)
.block();
});
}
public ExampleCustomer updateCustomer(String customerId, UpdateExampleCustomerRequest request) {
log.info("Updating customer in Example API: {}", customerId);
return executeWithResilience(() -> {
return put("/v1/customers/" + customerId, request, ExampleCustomer.class)
.block();
});
}
public void deleteCustomer(String customerId) {
log.info("Deleting customer from Example API: {}", customerId);
executeWithResilience(() -> {
delete("/v1/customers/" + customerId).block();
return null;
});
}
public List<ExampleCustomer> listCustomers(int page, int size) {
log.info("Listing customers from Example API: page={}, size={}", page, size);
return executeWithResilience(() -> {
String uri = String.format("/v1/customers?page=%d&size=%d", page, size);
ExampleApiResponse<List<ExampleCustomer>> response =
get(uri, new ParameterizedTypeReference<ExampleApiResponse<List<ExampleCustomer>>>() {})
.block();
return response != null ? response.getData() : List.of();
});
}
// ===== PROJECT OPERATIONS =====
public ExampleProject createProject(CreateExampleProjectRequest request) {
log.info("Creating project in Example API: {}", request.getName());
return executeWithResilience(() -> {
return post("/v1/projects", request, ExampleProject.class)
.block();
});
}
public ExampleProject getProject(String projectId) {
log.info("Fetching project from Example API: {}", projectId);
return executeWithResilience(() -> {
return get("/v1/projects/" + projectId, ExampleProject.class)
.block();
});
}
public ExampleProject updateProject(String projectId, UpdateExampleProjectRequest request) {
log.info("Updating project in Example API: {}", projectId);
return executeWithResilience(() -> {
return put("/v1/projects/" + projectId, request, ExampleProject.class)
.block();
});
}
public void deleteProject(String projectId) {
log.info("Deleting project from Example API: {}", projectId);
executeWithResilience(() -> {
delete("/v1/projects/" + projectId).block();
return null;
});
}
public List<ExampleProject> listProjects(String customerId, int page, int size) {
log.info("Listing projects for customer: {}", customerId);
return executeWithResilience(() -> {
String uri = String.format("/v1/projects?customerId=%s&page=%d&size=%d",
customerId, page, size);
ExampleApiResponse<List<ExampleProject>> response =
get(uri, new ParameterizedTypeReference<ExampleApiResponse<List<ExampleProject>>>() {})
.block();
return response != null ? response.getData() : List.of();
});
}
// ===== AGREEMENT (MSA) OPERATIONS =====
public ExampleAgreement createAgreement(CreateExampleAgreementRequest request) {
log.info("Creating agreement in Example API for customer: {}", request.getCustomerId());
return executeWithResilience(() -> {
return post("/v1/agreements", request, ExampleAgreement.class)
.block();
});
}
public ExampleAgreement getAgreement(String agreementId) {
log.info("Fetching agreement from Example API: {}", agreementId);
return executeWithResilience(() -> {
return get("/v1/agreements/" + agreementId, ExampleAgreement.class)
.block();
});
}
public ExampleAgreement updateAgreement(String agreementId, UpdateExampleAgreementRequest request) {
log.info("Updating agreement in Example API: {}", agreementId);
return executeWithResilience(() -> {
return put("/v1/agreements/" + agreementId, request, ExampleAgreement.class)
.block();
});
}
public void terminateAgreement(String agreementId) {
log.info("Terminating agreement in Example API: {}", agreementId);
executeWithResilience(() -> {
UpdateExampleAgreementRequest request = UpdateExampleAgreementRequest.builder()
.status("TERMINATED")
.build();
return put("/v1/agreements/" + agreementId, request, ExampleAgreement.class)
.block();
});
}
public List<ExampleAgreement> listAgreements(String customerId, String status) {
log.info("Listing agreements for customer: {}, status: {}", customerId, status);
return executeWithResilience(() -> {
String uri = String.format("/v1/agreements?customerId=%s&status=%s",
customerId, status != null ? status : "");
ExampleApiResponse<List<ExampleAgreement>> response =
get(uri, new ParameterizedTypeReference<ExampleApiResponse<List<ExampleAgreement>>>() {})
.block();
return response != null ? response.getData() : List.of();
});
}
// ===== SUBSCRIPTION OPERATIONS =====
public ExampleSubscription createSubscription(CreateExampleSubscriptionRequest request) {
log.info("Creating subscription in Example API for customer: {}", request.getCustomerId());
return executeWithResilience(() -> {
return post("/v1/subscriptions", request, ExampleSubscription.class)
.block();
});
}
public ExampleSubscription getSubscription(String subscriptionId) {
log.info("Fetching subscription from Example API: {}", subscriptionId);
return executeWithResilience(() -> {
return get("/v1/subscriptions/" + subscriptionId, ExampleSubscription.class)
.block();
});
}
public ExampleSubscription updateSubscription(String subscriptionId, UpdateExampleSubscriptionRequest request) {
log.info("Updating subscription in Example API: {}", subscriptionId);
return executeWithResilience(() -> {
return put("/v1/subscriptions/" + subscriptionId, request, ExampleSubscription.class)
.block();
});
}
public ExampleSubscription cancelSubscription(String subscriptionId, boolean cancelImmediately) {
log.info("Canceling subscription in Example API: {}, immediate: {}",
subscriptionId, cancelImmediately);
return executeWithResilience(() -> {
if (cancelImmediately) {
return delete("/v1/subscriptions/" + subscriptionId)
.then(Mono.empty())
.block();
} else {
UpdateExampleSubscriptionRequest request = UpdateExampleSubscriptionRequest.builder()
.cancelAtPeriodEnd(true)
.build();
return put("/v1/subscriptions/" + subscriptionId, request, ExampleSubscription.class)
.block();
}
});
}
public List<ExampleSubscription> listSubscriptions(String customerId, String status) {
log.info("Listing subscriptions for customer: {}, status: {}", customerId, status);
return executeWithResilience(() -> {
String uri = String.format("/v1/subscriptions?customerId=%s&status=%s",
customerId, status != null ? status : "");
ExampleApiResponse<List<ExampleSubscription>> response =
get(uri, new ParameterizedTypeReference<ExampleApiResponse<List<ExampleSubscription>>>() {})
.block();
return response != null ? response.getData() : List.of();
});
}
// ===== PRODUCT OPERATIONS =====
public ExampleProduct createProduct(CreateExampleProductRequest request) {
log.info("Creating product in Example API: {}", request.getName());
return executeWithResilience(() -> {
return post("/v1/products", request, ExampleProduct.class)
.block();
});
}
public ExampleProduct getProduct(String productId) {
log.info("Fetching product from Example API: {}", productId);
return executeWithResilience(() -> {
return get("/v1/products/" + productId, ExampleProduct.class)
.block();
});
}
public ExampleProduct getProductBySku(String sku) {
log.info("Fetching product by SKU from Example API: {}", sku);
return executeWithResilience(() -> {
return get("/v1/products/sku/" + sku, ExampleProduct.class)
.block();
});
}
public ExampleProduct updateProduct(String productId, UpdateExampleProductRequest request) {
log.info("Updating product in Example API: {}", productId);
return executeWithResilience(() -> {
return put("/v1/products/" + productId, request, ExampleProduct.class)
.block();
});
}
public void deleteProduct(String productId) {
log.info("Deleting product from Example API: {}", productId);
executeWithResilience(() -> {
delete("/v1/products/" + productId).block();
return null;
});
}
public List<ExampleProduct> listProducts(String productType, Boolean active, int page, int size) {
log.info("Listing products: type={}, active={}", productType, active);
return executeWithResilience(() -> {
String uri = String.format("/v1/products?type=%s&active=%s&page=%d&size=%d",
productType != null ? productType : "",
active != null ? active : "",
page, size);
ExampleApiResponse<List<ExampleProduct>> response =
get(uri, new ParameterizedTypeReference<ExampleApiResponse<List<ExampleProduct>>>() {})
.block();
return response != null ? response.getData() : List.of();
});
}
// ===== LICENSE OPERATIONS =====
public ExampleLicense createLicense(CreateExampleLicenseRequest request) {
log.info("Creating license in Example API for customer: {}", request.getCustomerId());
return executeWithResilience(() -> {
return post("/v1/licenses", request, ExampleLicense.class)
.block();
});
}
public ExampleLicense getLicense(String licenseId) {
log.info("Fetching license from Example API: {}", licenseId);
return executeWithResilience(() -> {
return get("/v1/licenses/" + licenseId, ExampleLicense.class)
.block();
});
}
public ExampleLicense getLicenseByKey(String licenseKey) {
log.info("Fetching license by key from Example API");
return executeWithResilience(() -> {
return get("/v1/licenses/key/" + licenseKey, ExampleLicense.class)
.block();
});
}
public ExampleLicense updateLicense(String licenseId, UpdateExampleLicenseRequest request) {
log.info("Updating license in Example API: {}", licenseId);
return executeWithResilience(() -> {
return put("/v1/licenses/" + licenseId, request, ExampleLicense.class)
.block();
});
}
public ExampleLicense activateLicense(String licenseId, String machineId) {
log.info("Activating license in Example API: {}", licenseId);
return executeWithResilience(() -> {
Map<String, String> request = Map.of("machineId", machineId);
return post("/v1/licenses/" + licenseId + "/activate", request, ExampleLicense.class)
.block();
});
}
public void revokeLicense(String licenseId, String reason) {
log.info("Revoking license in Example API: {}", licenseId);
executeWithResilience(() -> {
UpdateExampleLicenseRequest request = UpdateExampleLicenseRequest.builder()
.status("REVOKED")
.build();
return put("/v1/licenses/" + licenseId, request, ExampleLicense.class)
.block();
});
}
public List<ExampleLicense> listLicenses(String customerId, String status) {
log.info("Listing licenses for customer: {}, status: {}", customerId, status);
return executeWithResilience(() -> {
String uri = String.format("/v1/licenses?customerId=%s&status=%s",
customerId, status != null ? status : "");
ExampleApiResponse<List<ExampleLicense>> response =
get(uri, new ParameterizedTypeReference<ExampleApiResponse<List<ExampleLicense>>>() {})
.block();
return response != null ? response.getData() : List.of();
});
}
// ===== QUOTE OPERATIONS =====
public ExampleQuote createQuote(CreateExampleQuoteRequest request) {
log.info("Creating quote in Example API for customer: {}", request.getCustomerId());
return executeWithResilience(() -> {
return post("/v1/quotes", request, ExampleQuote.class)
.block();
});
}
public ExampleQuote getQuote(String quoteId) {
log.info("Fetching quote from Example API: {}", quoteId);
return executeWithResilience(() -> {
return get("/v1/quotes/" + quoteId, ExampleQuote.class)
.block();
});
}
public ExampleQuote updateQuote(String quoteId, UpdateExampleQuoteRequest request) {
log.info("Updating quote in Example API: {}", quoteId);
return executeWithResilience(() -> {
return put("/v1/quotes/" + quoteId, request, ExampleQuote.class)
.block();
});
}
public ExampleQuote acceptQuote(String quoteId) {
log.info("Accepting quote in Example API: {}", quoteId);
return executeWithResilience(() -> {
return post("/v1/quotes/" + quoteId + "/accept", null, ExampleQuote.class)
.block();
});
}
public ExampleQuote rejectQuote(String quoteId, String reason) {
log.info("Rejecting quote in Example API: {}", quoteId);
return executeWithResilience(() -> {
Map<String, String> request = Map.of("reason", reason != null ? reason : "");
return post("/v1/quotes/" + quoteId + "/reject", request, ExampleQuote.class)
.block();
});
}
public List<ExampleQuote> listQuotes(String customerId, String status) {
log.info("Listing quotes for customer: {}, status: {}", customerId, status);
return executeWithResilience(() -> {
String uri = String.format("/v1/quotes?customerId=%s&status=%s",
customerId, status != null ? status : "");
ExampleApiResponse<List<ExampleQuote>> response =
get(uri, new ParameterizedTypeReference<ExampleApiResponse<List<ExampleQuote>>>() {})
.block();
return response != null ? response.getData() : List.of();
});
}
// ===== ORDER OPERATIONS =====
public ExampleOrder createOrder(CreateExampleOrderRequest request) {
log.info("Creating order in Example API for customer: {}", request.getCustomerId());
return executeWithResilience(() -> {
return post("/v1/orders", request, ExampleOrder.class)
.block();
});
}
public ExampleOrder getOrder(String orderId) {
log.info("Fetching order from Example API: {}", orderId);
return executeWithResilience(() -> {
return get("/v1/orders/" + orderId, ExampleOrder.class)
.block();
});
}
public ExampleOrder updateOrder(String orderId, UpdateExampleOrderRequest request) {
log.info("Updating order in Example API: {}", orderId);
return executeWithResilience(() -> {
return put("/v1/orders/" + orderId, request, ExampleOrder.class)
.block();
});
}
public ExampleOrder fulfillOrder(String orderId, String trackingNumber) {
log.info("Fulfilling order in Example API: {}", orderId);
return executeWithResilience(() -> {
UpdateExampleOrderRequest request = UpdateExampleOrderRequest.builder()
.status("FULFILLED")
.trackingNumber(trackingNumber)
.build();
return put("/v1/orders/" + orderId, request, ExampleOrder.class)
.block();
});
}
public ExampleOrder cancelOrder(String orderId, String reason) {
log.info("Canceling order in Example API: {}", orderId);
return executeWithResilience(() -> {
UpdateExampleOrderRequest request = UpdateExampleOrderRequest.builder()
.status("CANCELED")
.build();
return put("/v1/orders/" + orderId, request, ExampleOrder.class)
.block();
});
}
public List<ExampleOrder> listOrders(String customerId, String status, int page, int size) {
log.info("Listing orders for customer: {}, status: {}", customerId, status);
return executeWithResilience(() -> {
String uri = String.format("/v1/orders?customerId=%s&status=%s&page=%d&size=%d",
customerId, status != null ? status : "", page, size);
ExampleApiResponse<List<ExampleOrder>> response =
get(uri, new ParameterizedTypeReference<ExampleApiResponse<List<ExampleOrder>>>() {})
.block();
return response != null ? response.getData() : List.of();
});
}
private <T> T executeWithResilience(Supplier<T> operation) {
return rateLimiter.executeSupplier(() ->
circuitBreaker.executeSupplier(() -> {
addAuthenticationHeader();
return operation.get();
})
);
}
private void addAuthenticationHeader() {
// Add Bearer token to request
String token = authProvider.getAccessToken();
// WebClient will use this via filter
}
}
Usage Examples
Webhook Handler
// filepath: /Users/jetstart/dev/jetrev/ink/src/main/java/com/jetrev/ink/integration/example/webhook/ExampleWebhookController.java
@RestController
@RequestMapping("/api/webhooks/example")
@Slf4j
@RequiredArgsConstructor
@ConditionalOnProperty(name = "integration.example.enabled", havingValue = "true")
public class ExampleWebhookController {
private final ExampleWebhookService webhookService;
private final ExampleIntegrationProperties properties;
@PostMapping
public ResponseEntity<Void> handleWebhook(
@RequestHeader("X-Example-Signature") String signature,
@RequestBody String payload) {
log.info("Received Example webhook");
// Verify signature
if (!webhookService.verifySignature(payload, signature)) {
log.warn("Invalid webhook signature");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
try {
ExampleWebhookEvent event = objectMapper.readValue(payload, ExampleWebhookEvent.class);
webhookService.processWebhook(event);
return ResponseEntity.ok().build();
} catch (Exception e) {
log.error("Failed to process webhook", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
}
@Service
@Slf4j
@RequiredArgsConstructor
public class ExampleWebhookService {
private final ExampleIntegrationProperties properties;
public boolean verifySignature(String payload, String signature) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(
properties.getApiSecret().getBytes(StandardCharsets.UTF_8),
"HmacSHA256"
);
mac.init(secretKey);
byte[] hash = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
String expectedSignature = Hex.encodeHexString(hash);
return signature.equals(expectedSignature);
} catch (Exception e) {
log.error("Signature verification failed", e);
return false;
}
}
public void processWebhook(ExampleWebhookEvent event) {
log.info("Processing webhook event: {}", event.getType());
switch (event.getType()) {
case "customer.created":
handleCustomerCreated(event);
break;
case "customer.updated":
handleCustomerUpdated(event);
break;
case "customer.deleted":
handleCustomerDeleted(event);
break;
default:
log.warn("Unknown webhook event type: {}", event.getType());
}
}
private void handleCustomerCreated(ExampleWebhookEvent event) {
// Handle customer creation
}
private void handleCustomerUpdated(ExampleWebhookEvent event) {
// Handle customer update
}
private void handleCustomerDeleted(ExampleWebhookEvent event) {
// Handle customer deletion
}
}
Verification
Integration Tests
// filepath: /Users/jetstart/dev/jetrev/ink/src/test/java/com/jetrev/ink/integration/example/ExampleClientTest.java
@SpringBootTest
@ActiveProfiles("test")
class ExampleClientTest {
@Autowired
private ExampleClient exampleClient;
private WireMockServer wireMockServer;
@BeforeEach
void setUp() {
wireMockServer = new WireMockServer(8090);
wireMockServer.start();
WireMock.configureFor("localhost", 8090);
// Stub OAuth token endpoint
stubFor(post(urlEqualTo("/oauth/token"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("""
{
"access_token": "test_token",
"token_type": "Bearer",
"expires_in": 3600
}
""")));
}
@AfterEach
void tearDown() {
wireMockServer.stop();
}
@Test
void shouldCreateCustomerSuccessfully() {
// Given
stubFor(post(urlEqualTo("/v1/customers"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("""
{
"id": "cus_123",
"email": "test@example.com",
"name": "Test User",
"createdAt": "2024-01-01T00:00:00Z"
}
""")));
CreateExampleCustomerRequest request = CreateExampleCustomerRequest.builder()
.email("test@example.com")
.name("Test User")
.build();
// When
ExampleCustomer customer = exampleClient.createCustomer(request);
// Then
assertThat(customer).isNotNull();
assertThat(customer.getId()).isEqualTo("cus_123");
assertThat(customer.getEmail()).isEqualTo("test@example.com");
verify(postRequestedFor(urlEqualTo("/v1/customers"))
.withHeader("Authorization", matching("Bearer .*")));
}
}
Troubleshooting
Authentication Issues
// Add debug logging for authentication
@Slf4j
public class ExampleAuthenticationProvider {
private void refreshToken() {
log.debug("Current token: {}", accessToken != null ? "present" : "null");
log.debug("Token expires at: {}", tokenExpiresAt);
// ...existing code...
log.debug("New token obtained, expires in: {} seconds", response.getExpiresIn());
}
}
Rate Limiting
# Adjust rate limiting configuration
resilience4j:
ratelimiter:
instances:
example:
limitForPeriod: 10
limitRefreshPeriod: 1s
timeoutDuration: 5s
Best Practices
- Configuration Management: Use environment-specific properties
- Authentication Caching: Cache tokens to avoid unnecessary requests
- Error Handling: Implement proper error handling and retries
- Webhook Security: Always verify webhook signatures
- Idempotency: Use idempotency keys for create operations
- Testing: Write comprehensive integration tests with WireMock
- Monitoring: Add metrics and logging for integration health
- Documentation: Document all endpoints, request/response formats
- Versioning: Handle API version changes gracefully
- Backward Compatibility: Support multiple API versions if needed
Documentation Template
# Example Integration
## Overview
Brief description of the integration and its purpose.
## Configuration
List all required environment variables and configuration properties.
## Authentication
Explain the authentication mechanism (OAuth2, API key, etc.).
## Supported Operations
### Customer Operations
- Create Customer
- Get Customer
- Update Customer
- Delete Customer
- List Customers
### Project Operations
- Create Project
- Get Project
- Update Project
- Delete Project
- List Projects by Customer
### Agreement (MSA) Operations
- Create Agreement
- Get Agreement
- Update Agreement
- Terminate Agreement
- List Agreements by Customer
### Subscription Operations
- Create Subscription
- Get Subscription
- Update Subscription
- Cancel Subscription (immediate or at period end)
- List Subscriptions by Customer
### Product Operations
- Create Product
- Get Product by ID
- Get Product by SKU
- Update Product
- Delete Product
- List Products (with filters)
### License Operations
- Create License
- Get License by ID
- Get License by Key
- Update License
- Activate License
- Revoke License
- List Licenses by Customer
### Quote Operations
- Create Quote
- Get Quote
- Update Quote
- Accept Quote
- Reject Quote
- List Quotes by Customer
### Order Operations
- Create Order
- Get Order
- Update Order
- Fulfill Order
- Cancel Order
- List Orders by Customer
## Webhook Events
- `customer.created`, `customer.updated`, `customer.deleted`
- `project.created`, `project.updated`, `project.deleted`
- `agreement.created`, `agreement.updated`, `agreement.terminated`
- `subscription.created`, `subscription.updated`, `subscription.canceled`
- `product.created`, `product.updated`, `product.deleted`
- `license.created`, `license.activated`, `license.revoked`
- `quote.created`, `quote.accepted`, `quote.rejected`
- `order.created`, `order.paid`, `order.fulfilled`, `order.canceled`
## Error Handling
Common errors and how to handle them.
## Rate Limits
API rate limits and how they're handled.
## Testing
How to test the integration locally.
## Troubleshooting
Common issues and solutions.
Related Documentation
- Integration Patterns
- Stripe Integration Deep Dive
- QuickBooks Integration Deep Dive
- Service Layer Architecture
Additional Resources
Next Steps: Review Stripe Integration Deep Dive for a complete example implementation.