Merge branch 'EC-21-Official-SDKs'
Refers: Evercatch/evercatch-board#21
This commit was merged in pull request #1.
This commit is contained in:
13
.gitignore
vendored
13
.gitignore
vendored
@@ -3,6 +3,19 @@
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Java / Maven / Gradle
|
||||
target/
|
||||
*.class
|
||||
*.jar
|
||||
*.war
|
||||
*.ear
|
||||
.gradle/
|
||||
build/
|
||||
!gradle-wrapper.jar
|
||||
.gradletasknamecache
|
||||
.mvn/timing.properties
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
229
README.md
229
README.md
@@ -1,49 +1,218 @@
|
||||
# 📦 Repository Name
|
||||
# Evercatch Java SDK
|
||||
|
||||
> Short one-line description of what this repository does.
|
||||
Official Java SDK for [Evercatch](https://evercatch.dev) — webhook infrastructure platform.
|
||||
|
||||
---
|
||||
|
||||
## 🧭 Overview
|
||||
|
||||
Describe what this service/module is responsible for within the Evercatch platform.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Tech Stack
|
||||
## Tech Stack
|
||||
|
||||
| Layer | Technology |
|
||||
| :--- | :--- |
|
||||
| Language | — |
|
||||
| Framework | — |
|
||||
| Key Dependencies | — |
|
||||
| Language | Java 11+ |
|
||||
| HTTP client | OkHttp 4 |
|
||||
| JSON | Gson |
|
||||
| Build | Maven / Gradle |
|
||||
| Spring Boot | Auto-configuration (optional) |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Getting Started
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
### Maven
|
||||
|
||||
- Docker & Docker Compose
|
||||
- Node.js / Python (specify version)
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>dev.evercatch</groupId>
|
||||
<artifactId>evercatch-java</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
### Local Development
|
||||
### Gradle
|
||||
|
||||
```bash
|
||||
# Clone the repo
|
||||
git clone https://git.psmattas.com/Evercatch/REPO_NAME.git
|
||||
cd REPO_NAME
|
||||
|
||||
# Copy environment variables
|
||||
cp .env.example .env
|
||||
|
||||
# Start services
|
||||
docker compose up -d
|
||||
```gradle
|
||||
implementation 'dev.evercatch:evercatch-java:1.0.0'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🌿 Branching & Commits
|
||||
## Quick Start
|
||||
|
||||
```java
|
||||
import dev.evercatch.EvercatchClient;
|
||||
import dev.evercatch.model.*;
|
||||
import java.util.List;
|
||||
|
||||
EvercatchClient client = new EvercatchClient("ec_live_abc123");
|
||||
|
||||
// Create a destination
|
||||
Destination dest = client.createDestination(
|
||||
CreateDestinationRequest.builder()
|
||||
.name("Production")
|
||||
.url("https://myapp.com/webhooks")
|
||||
.providers(List.of("stripe", "sendgrid"))
|
||||
.eventTypes(List.of("payment.*", "email.delivered"))
|
||||
.build()
|
||||
);
|
||||
System.out.println("Created: " + dest.getId());
|
||||
|
||||
// List events
|
||||
List<Event> events = client.listEvents(
|
||||
ListEventsRequest.builder()
|
||||
.provider("stripe")
|
||||
.limit(50)
|
||||
.build()
|
||||
);
|
||||
System.out.println("Events: " + events.size());
|
||||
|
||||
// Check usage
|
||||
Usage usage = client.getUsage();
|
||||
System.out.println("Plan: " + usage.getPlan());
|
||||
System.out.println("Events this month: " + usage.getEventsThisMonth() + "/" + usage.getEventsLimit());
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
### Destinations
|
||||
|
||||
| Method | Description |
|
||||
| :--- | :--- |
|
||||
| `createDestination(request)` | Register a new webhook destination |
|
||||
| `listDestinations()` | List all destinations |
|
||||
| `getDestination(id)` | Get a destination by ID |
|
||||
| `deleteDestination(id)` | Delete a destination |
|
||||
|
||||
### Events
|
||||
|
||||
| Method | Description |
|
||||
| :--- | :--- |
|
||||
| `listEvents(request)` | List events with optional filters |
|
||||
| `getEvent(id)` | Get an event by ID |
|
||||
| `replayEvent(eventId, destinationIds)` | Replay an event (Studio+ only) |
|
||||
|
||||
### Account
|
||||
|
||||
| Method | Description |
|
||||
| :--- | :--- |
|
||||
| `getUsage()` | Get current account usage statistics |
|
||||
|
||||
---
|
||||
|
||||
## Spring Boot Integration
|
||||
|
||||
Add the dependency, then set your API key in `application.properties`:
|
||||
|
||||
```properties
|
||||
evercatch.api-key=ec_live_abc123
|
||||
# Optional: override the default API base URL
|
||||
# evercatch.base-url=https://api.evercatch.dev/v1
|
||||
```
|
||||
|
||||
Or in `application.yml`:
|
||||
|
||||
```yaml
|
||||
evercatch:
|
||||
api-key: ec_live_abc123
|
||||
```
|
||||
|
||||
An `EvercatchClient` bean is registered automatically — inject it wherever you need it:
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class WebhookService {
|
||||
|
||||
private final EvercatchClient evercatch;
|
||||
|
||||
public WebhookService(EvercatchClient evercatch) {
|
||||
this.evercatch = evercatch;
|
||||
}
|
||||
|
||||
public void process(String eventId) throws EvercatchException {
|
||||
Event event = evercatch.getEvent(eventId);
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Override the auto-configured bean by declaring your own:
|
||||
|
||||
```java
|
||||
@Bean
|
||||
public EvercatchClient evercatchClient() {
|
||||
return new EvercatchClient("ec_live_abc123", "https://custom-host/v1");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
All methods throw `EvercatchException` (a checked exception). The status code is available via `getStatusCode()`:
|
||||
|
||||
```java
|
||||
try {
|
||||
client.replayEvent(eventId, List.of(destId));
|
||||
} catch (EvercatchException e) {
|
||||
if (e.getStatusCode() == 402) {
|
||||
System.out.println("Upgrade to Studio to replay events.");
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Building from Source
|
||||
|
||||
```bash
|
||||
# Maven
|
||||
mvn clean package
|
||||
|
||||
# Run tests
|
||||
mvn test
|
||||
|
||||
# Gradle
|
||||
./gradlew build
|
||||
|
||||
# Run tests
|
||||
./gradlew test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Publishing
|
||||
|
||||
### Maven Central
|
||||
|
||||
```bash
|
||||
# Build, sign, and stage
|
||||
mvn clean deploy -P release
|
||||
|
||||
# Release from staging
|
||||
mvn nexus-staging:release
|
||||
```
|
||||
|
||||
### GitHub Packages
|
||||
|
||||
```bash
|
||||
mvn deploy
|
||||
# or
|
||||
./gradlew publish
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
- Java 11 or higher
|
||||
- Maven 3.6+ or Gradle 7.0+
|
||||
|
||||
---
|
||||
|
||||
## Branching & Commits
|
||||
|
||||
All work follows the Evercatch contribution guide defined in the org README.
|
||||
|
||||
@@ -54,7 +223,7 @@ See [Evercatch Org README](https://git.psmattas.com/Evercatch) for full conventi
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ CI/CD
|
||||
## CI/CD
|
||||
|
||||
Automated via Jenkins. Merges to `main` trigger staging deployments.
|
||||
|
||||
@@ -66,7 +235,7 @@ Automated via Jenkins. Merges to `main` trigger staging deployments.
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
## License
|
||||
|
||||
**Copyright © 2026 Evercatch.**
|
||||
Proprietary and confidential. Unauthorised distribution is strictly prohibited.
|
||||
|
||||
101
build.gradle
Normal file
101
build.gradle
Normal file
@@ -0,0 +1,101 @@
|
||||
plugins {
|
||||
id 'java-library'
|
||||
id 'maven-publish'
|
||||
id 'signing'
|
||||
}
|
||||
|
||||
group = 'dev.evercatch'
|
||||
version = '1.0.0'
|
||||
description = 'Official Java SDK for Evercatch webhook infrastructure platform'
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
withSourcesJar()
|
||||
withJavadocJar()
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
ext {
|
||||
okhttpVersion = '4.12.0'
|
||||
gsonVersion = '2.10.1'
|
||||
junitVersion = '5.10.1'
|
||||
mockitoVersion = '5.8.0'
|
||||
springBootVersion = '3.2.1'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// HTTP Client
|
||||
api "com.squareup.okhttp3:okhttp:${okhttpVersion}"
|
||||
|
||||
// JSON Processing
|
||||
api "com.google.code.gson:gson:${gsonVersion}"
|
||||
|
||||
// Spring Boot AutoConfiguration (optional — consumers pull it in themselves)
|
||||
compileOnly "org.springframework.boot:spring-boot-autoconfigure:${springBootVersion}"
|
||||
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor:${springBootVersion}"
|
||||
|
||||
// Testing
|
||||
testImplementation "org.junit.jupiter:junit-jupiter:${junitVersion}"
|
||||
testImplementation "org.mockito:mockito-core:${mockitoVersion}"
|
||||
testImplementation "com.squareup.okhttp3:mockwebserver:${okhttpVersion}"
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
mavenJava(MavenPublication) {
|
||||
from components.java
|
||||
|
||||
pom {
|
||||
name = 'Evercatch Java SDK'
|
||||
description = project.description
|
||||
url = 'https://evercatch.dev'
|
||||
|
||||
licenses {
|
||||
license {
|
||||
name = 'MIT License'
|
||||
url = 'https://opensource.org/licenses/MIT'
|
||||
}
|
||||
}
|
||||
|
||||
developers {
|
||||
developer {
|
||||
name = 'Evercatch Team'
|
||||
email = 'support@evercatch.dev'
|
||||
organization = 'Evercatch'
|
||||
organizationUrl = 'https://evercatch.dev'
|
||||
}
|
||||
}
|
||||
|
||||
scm {
|
||||
connection = 'scm:git:git://git.psmattas.com/evercatch/evercatch-java.git'
|
||||
developerConnection = 'scm:git:ssh://git.psmattas.com/evercatch/evercatch-java.git'
|
||||
url = 'https://git.psmattas.com/evercatch/evercatch-java'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
// Maven Central via Sonatype Central Publisher Portal
|
||||
// Publishing is handled by the central-publishing-maven-plugin in pom.xml.
|
||||
// Use `mvn deploy -P release` to publish.
|
||||
}
|
||||
}
|
||||
|
||||
signing {
|
||||
def signingKey = findProperty('signingKey') ?: System.getenv('GPG_SIGNING_KEY')
|
||||
def signingPassword = findProperty('signingPassword') ?: System.getenv('GPG_SIGNING_PASSWORD')
|
||||
if (signingKey) {
|
||||
useInMemoryPgpKeys(signingKey, signingPassword)
|
||||
}
|
||||
sign publishing.publications.mavenJava
|
||||
}
|
||||
180
pom.xml
Normal file
180
pom.xml
Normal file
@@ -0,0 +1,180 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>dev.evercatch</groupId>
|
||||
<artifactId>evercatch-java</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>Evercatch Java SDK</name>
|
||||
<description>Official Java SDK for Evercatch webhook infrastructure platform</description>
|
||||
<url>https://evercatch.dev</url>
|
||||
|
||||
<licenses>
|
||||
<license>
|
||||
<name>MIT License</name>
|
||||
<url>https://opensource.org/licenses/MIT</url>
|
||||
</license>
|
||||
</licenses>
|
||||
|
||||
<developers>
|
||||
<developer>
|
||||
<name>Evercatch Team</name>
|
||||
<email>support@evercatch.dev</email>
|
||||
<organization>Evercatch</organization>
|
||||
<organizationUrl>https://evercatch.dev</organizationUrl>
|
||||
</developer>
|
||||
</developers>
|
||||
|
||||
<scm>
|
||||
<connection>scm:git:git://git.psmattas.com/evercatch/evercatch-java.git</connection>
|
||||
<developerConnection>scm:git:ssh://git.psmattas.com/evercatch/evercatch-java.git</developerConnection>
|
||||
<url>https://git.psmattas.com/evercatch/evercatch-java</url>
|
||||
</scm>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<okhttp.version>4.12.0</okhttp.version>
|
||||
<gson.version>2.10.1</gson.version>
|
||||
<junit.version>5.10.1</junit.version>
|
||||
<mockito.version>5.8.0</mockito.version>
|
||||
<spring-boot.version>3.2.1</spring-boot.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- HTTP Client -->
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
<version>${okhttp.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JSON Processing -->
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>${gson.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot AutoConfiguration (optional) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-autoconfigure</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Testing -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>${mockito.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>mockwebserver</artifactId>
|
||||
<version>${okhttp.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
</plugin>
|
||||
|
||||
<!-- Sonatype Central Publisher Portal -->
|
||||
<plugin>
|
||||
<groupId>org.sonatype.central</groupId>
|
||||
<artifactId>central-publishing-maven-plugin</artifactId>
|
||||
<version>0.9.0</version>
|
||||
<extensions>true</extensions>
|
||||
<configuration>
|
||||
<publishingServerId>central</publishingServerId>
|
||||
<autoPublish>true</autoPublish>
|
||||
<waitUntil>published</waitUntil>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>3.3.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-sources</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>3.6.3</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-javadocs</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.2.3</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>release</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-gpg-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>sign-artifacts</id>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>sign</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
1
settings.gradle
Normal file
1
settings.gradle
Normal file
@@ -0,0 +1 @@
|
||||
rootProject.name = 'evercatch-java'
|
||||
264
src/main/java/dev/evercatch/EvercatchClient.java
Normal file
264
src/main/java/dev/evercatch/EvercatchClient.java
Normal file
@@ -0,0 +1,264 @@
|
||||
package dev.evercatch;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import dev.evercatch.http.HttpClient;
|
||||
import dev.evercatch.model.*;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Evercatch API client for Java.
|
||||
*
|
||||
* <p>Example usage:</p>
|
||||
* <pre>{@code
|
||||
* EvercatchClient client = new EvercatchClient("ec_live_abc123");
|
||||
*
|
||||
* // Create a destination
|
||||
* Destination dest = client.createDestination(
|
||||
* CreateDestinationRequest.builder()
|
||||
* .name("Production")
|
||||
* .url("https://myapp.com/webhooks")
|
||||
* .providers(List.of("stripe", "sendgrid"))
|
||||
* .build()
|
||||
* );
|
||||
*
|
||||
* // List recent events
|
||||
* List<Event> events = client.listEvents(
|
||||
* ListEventsRequest.builder()
|
||||
* .provider("stripe")
|
||||
* .limit(50)
|
||||
* .build()
|
||||
* );
|
||||
* }</pre>
|
||||
*/
|
||||
public class EvercatchClient {
|
||||
|
||||
private static final String DEFAULT_BASE_URL = "https://api.evercatch.dev/v1";
|
||||
private static final MediaType JSON_MEDIA_TYPE = MediaType.parse("application/json; charset=utf-8");
|
||||
|
||||
private final String baseUrl;
|
||||
private final HttpClient httpClient;
|
||||
private final Gson gson;
|
||||
|
||||
/**
|
||||
* Creates a client using the default API base URL.
|
||||
*
|
||||
* @param apiKey your Evercatch API key (starts with {@code ec_live_} or {@code ec_test_})
|
||||
*/
|
||||
public EvercatchClient(String apiKey) {
|
||||
this(apiKey, DEFAULT_BASE_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a client with a custom base URL (useful for self-hosted instances or testing).
|
||||
*
|
||||
* @param apiKey your Evercatch API key
|
||||
* @param baseUrl custom API base URL
|
||||
*/
|
||||
public EvercatchClient(String apiKey, String baseUrl) {
|
||||
if (apiKey == null || apiKey.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("API key cannot be null or empty");
|
||||
}
|
||||
this.baseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
|
||||
this.httpClient = new HttpClient(apiKey);
|
||||
this.gson = new GsonBuilder()
|
||||
.setDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
|
||||
.create();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Destinations
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates a new webhook destination.
|
||||
*
|
||||
* @param request destination configuration
|
||||
* @return the created {@link Destination}
|
||||
* @throws EvercatchException if the API request fails
|
||||
*/
|
||||
public Destination createDestination(CreateDestinationRequest request) throws EvercatchException {
|
||||
String url = baseUrl + "/destinations";
|
||||
RequestBody body = RequestBody.create(gson.toJson(request), JSON_MEDIA_TYPE);
|
||||
|
||||
try (Response response = httpClient.post(url, body)) {
|
||||
handleError(response, "create destination");
|
||||
return gson.fromJson(response.body().string(), Destination.class);
|
||||
} catch (IOException e) {
|
||||
throw new EvercatchException("Network error creating destination", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all webhook destinations for the account.
|
||||
*
|
||||
* @return list of destinations
|
||||
* @throws EvercatchException if the API request fails
|
||||
*/
|
||||
public List<Destination> listDestinations() throws EvercatchException {
|
||||
String url = baseUrl + "/destinations";
|
||||
|
||||
try (Response response = httpClient.get(url)) {
|
||||
handleError(response, "list destinations");
|
||||
DestinationListResponse resp = gson.fromJson(
|
||||
response.body().string(), DestinationListResponse.class);
|
||||
return resp.getData();
|
||||
} catch (IOException e) {
|
||||
throw new EvercatchException("Network error listing destinations", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a single destination by ID.
|
||||
*
|
||||
* @param id destination ID
|
||||
* @return the matching {@link Destination}
|
||||
* @throws EvercatchException if the API request fails
|
||||
*/
|
||||
public Destination getDestination(String id) throws EvercatchException {
|
||||
String url = baseUrl + "/destinations/" + id;
|
||||
|
||||
try (Response response = httpClient.get(url)) {
|
||||
handleError(response, "get destination");
|
||||
return gson.fromJson(response.body().string(), Destination.class);
|
||||
} catch (IOException e) {
|
||||
throw new EvercatchException("Network error getting destination", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a destination.
|
||||
*
|
||||
* @param id destination ID
|
||||
* @throws EvercatchException if the API request fails
|
||||
*/
|
||||
public void deleteDestination(String id) throws EvercatchException {
|
||||
String url = baseUrl + "/destinations/" + id;
|
||||
|
||||
try (Response response = httpClient.delete(url)) {
|
||||
handleError(response, "delete destination");
|
||||
} catch (IOException e) {
|
||||
throw new EvercatchException("Network error deleting destination", e);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Events
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Lists webhook events with optional filters.
|
||||
*
|
||||
* @param request filter / pagination options (use {@code ListEventsRequest.builder().build()}
|
||||
* for defaults)
|
||||
* @return list of events
|
||||
* @throws EvercatchException if the API request fails
|
||||
*/
|
||||
public List<Event> listEvents(ListEventsRequest request) throws EvercatchException {
|
||||
StringBuilder url = new StringBuilder(baseUrl).append("/events?");
|
||||
|
||||
Map<String, String> params = new HashMap<>();
|
||||
if (request.getProvider() != null) params.put("provider", request.getProvider());
|
||||
if (request.getEventType() != null) params.put("event_type", request.getEventType());
|
||||
if (request.getStatus() != null) params.put("status", request.getStatus());
|
||||
if (request.getLimit() != null) params.put("limit", request.getLimit().toString());
|
||||
if (request.getCursor() != null) params.put("cursor", request.getCursor());
|
||||
|
||||
params.forEach((k, v) -> url.append(k).append("=").append(v).append("&"));
|
||||
|
||||
try (Response response = httpClient.get(url.toString())) {
|
||||
handleError(response, "list events");
|
||||
EventListResponse resp = gson.fromJson(
|
||||
response.body().string(), EventListResponse.class);
|
||||
return resp.getData();
|
||||
} catch (IOException e) {
|
||||
throw new EvercatchException("Network error listing events", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a single event by ID.
|
||||
*
|
||||
* @param id event ID
|
||||
* @return the matching {@link Event}
|
||||
* @throws EvercatchException if the API request fails
|
||||
*/
|
||||
public Event getEvent(String id) throws EvercatchException {
|
||||
String url = baseUrl + "/events/" + id;
|
||||
|
||||
try (Response response = httpClient.get(url)) {
|
||||
handleError(response, "get event");
|
||||
return gson.fromJson(response.body().string(), Event.class);
|
||||
} catch (IOException e) {
|
||||
throw new EvercatchException("Network error getting event", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replays an event to the specified destinations.
|
||||
*
|
||||
* <p>Requires the Studio or Enterprise plan.</p>
|
||||
*
|
||||
* @param eventId ID of the event to replay
|
||||
* @param destinationIds destination IDs to replay to
|
||||
* @throws EvercatchException if the API request fails or the plan does not support replay
|
||||
*/
|
||||
public void replayEvent(String eventId, List<String> destinationIds) throws EvercatchException {
|
||||
String url = baseUrl + "/events/" + eventId + "/replay";
|
||||
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
payload.put("destination_ids", destinationIds);
|
||||
|
||||
RequestBody body = RequestBody.create(gson.toJson(payload), JSON_MEDIA_TYPE);
|
||||
|
||||
try (Response response = httpClient.post(url, body)) {
|
||||
if (response.code() == 402) {
|
||||
throw new EvercatchException("Event replay requires Studio or Enterprise plan", 402);
|
||||
}
|
||||
handleError(response, "replay event");
|
||||
} catch (IOException e) {
|
||||
throw new EvercatchException("Network error replaying event", e);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Account
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns current account usage statistics.
|
||||
*
|
||||
* @return {@link Usage} summary
|
||||
* @throws EvercatchException if the API request fails
|
||||
*/
|
||||
public Usage getUsage() throws EvercatchException {
|
||||
String url = baseUrl + "/account/usage";
|
||||
|
||||
try (Response response = httpClient.get(url)) {
|
||||
handleError(response, "get usage");
|
||||
return gson.fromJson(response.body().string(), Usage.class);
|
||||
} catch (IOException e) {
|
||||
throw new EvercatchException("Network error getting usage", e);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void handleError(Response response, String operation) throws EvercatchException, IOException {
|
||||
if (!response.isSuccessful()) {
|
||||
String body = response.body() != null ? response.body().string() : "";
|
||||
throw new EvercatchException(
|
||||
String.format("Failed to %s (HTTP %d): %s", operation, response.code(), body),
|
||||
response.code());
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/main/java/dev/evercatch/EvercatchException.java
Normal file
35
src/main/java/dev/evercatch/EvercatchException.java
Normal file
@@ -0,0 +1,35 @@
|
||||
package dev.evercatch;
|
||||
|
||||
/**
|
||||
* Exception thrown when Evercatch API operations fail.
|
||||
*
|
||||
* <p>Wraps both HTTP-level errors (non-2xx responses) and network-level
|
||||
* I/O errors so callers only need to handle a single checked exception type.</p>
|
||||
*/
|
||||
public class EvercatchException extends Exception {
|
||||
|
||||
private final int statusCode;
|
||||
|
||||
public EvercatchException(String message) {
|
||||
super(message);
|
||||
this.statusCode = -1;
|
||||
}
|
||||
|
||||
public EvercatchException(String message, int statusCode) {
|
||||
super(message);
|
||||
this.statusCode = statusCode;
|
||||
}
|
||||
|
||||
public EvercatchException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.statusCode = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the HTTP status code that triggered this exception, or {@code -1}
|
||||
* if the failure was not caused by an HTTP response (e.g. a network error).
|
||||
*/
|
||||
public int getStatusCode() {
|
||||
return statusCode;
|
||||
}
|
||||
}
|
||||
76
src/main/java/dev/evercatch/http/HttpClient.java
Normal file
76
src/main/java/dev/evercatch/http/HttpClient.java
Normal file
@@ -0,0 +1,76 @@
|
||||
package dev.evercatch.http;
|
||||
|
||||
import okhttp3.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Thin OkHttp wrapper that attaches Evercatch authentication headers to every
|
||||
* outbound request.
|
||||
*/
|
||||
public class HttpClient {
|
||||
|
||||
private static final String SDK_VERSION = "1.0.0";
|
||||
private static final String USER_AGENT = "evercatch-java/" + SDK_VERSION;
|
||||
|
||||
private final OkHttpClient client;
|
||||
private final String apiKey;
|
||||
|
||||
public HttpClient(String apiKey) {
|
||||
this.apiKey = apiKey;
|
||||
this.client = new OkHttpClient.Builder()
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.writeTimeout(30, TimeUnit.SECONDS)
|
||||
.build();
|
||||
}
|
||||
|
||||
/** Package-private constructor for testing with a custom OkHttpClient. */
|
||||
HttpClient(String apiKey, OkHttpClient client) {
|
||||
this.apiKey = apiKey;
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
public Response get(String url) throws IOException {
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.header("X-API-Key", apiKey)
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.get()
|
||||
.build();
|
||||
return client.newCall(request).execute();
|
||||
}
|
||||
|
||||
public Response post(String url, RequestBody body) throws IOException {
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.header("X-API-Key", apiKey)
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.header("Content-Type", "application/json")
|
||||
.post(body)
|
||||
.build();
|
||||
return client.newCall(request).execute();
|
||||
}
|
||||
|
||||
public Response put(String url, RequestBody body) throws IOException {
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.header("X-API-Key", apiKey)
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.header("Content-Type", "application/json")
|
||||
.put(body)
|
||||
.build();
|
||||
return client.newCall(request).execute();
|
||||
}
|
||||
|
||||
public Response delete(String url) throws IOException {
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.header("X-API-Key", apiKey)
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.delete()
|
||||
.build();
|
||||
return client.newCall(request).execute();
|
||||
}
|
||||
}
|
||||
100
src/main/java/dev/evercatch/model/CreateDestinationRequest.java
Normal file
100
src/main/java/dev/evercatch/model/CreateDestinationRequest.java
Normal file
@@ -0,0 +1,100 @@
|
||||
package dev.evercatch.model;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Request object for creating a new webhook destination.
|
||||
*
|
||||
* <pre>{@code
|
||||
* CreateDestinationRequest req = CreateDestinationRequest.builder()
|
||||
* .name("Production")
|
||||
* .url("https://myapp.com/webhooks")
|
||||
* .providers(List.of("stripe", "sendgrid"))
|
||||
* .eventTypes(List.of("payment.*", "email.delivered"))
|
||||
* .build();
|
||||
* }</pre>
|
||||
*/
|
||||
public class CreateDestinationRequest {
|
||||
|
||||
private final String name;
|
||||
private final String url;
|
||||
private final List<String> providers;
|
||||
|
||||
@SerializedName("event_types")
|
||||
private final List<String> eventTypes;
|
||||
|
||||
private final Map<String, String> headers;
|
||||
|
||||
private CreateDestinationRequest(Builder builder) {
|
||||
this.name = builder.name;
|
||||
this.url = builder.url;
|
||||
this.providers = builder.providers;
|
||||
this.eventTypes = builder.eventTypes;
|
||||
this.headers = builder.headers;
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public String getName() { return name; }
|
||||
public String getUrl() { return url; }
|
||||
public List<String> getProviders() { return providers; }
|
||||
public List<String> getEventTypes() { return eventTypes; }
|
||||
public Map<String, String> getHeaders() { return headers; }
|
||||
|
||||
public static class Builder {
|
||||
private String name;
|
||||
private String url;
|
||||
private List<String> providers;
|
||||
private List<String> eventTypes;
|
||||
private Map<String, String> headers = new HashMap<>();
|
||||
|
||||
public Builder name(String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder url(String url) {
|
||||
this.url = url;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder providers(List<String> providers) {
|
||||
this.providers = providers;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder eventTypes(List<String> eventTypes) {
|
||||
this.eventTypes = eventTypes;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder headers(Map<String, String> headers) {
|
||||
this.headers = headers;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder addHeader(String key, String value) {
|
||||
this.headers.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public CreateDestinationRequest build() {
|
||||
if (name == null || name.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("name is required");
|
||||
}
|
||||
if (url == null || url.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("url is required");
|
||||
}
|
||||
if (providers == null || providers.isEmpty()) {
|
||||
throw new IllegalArgumentException("at least one provider is required");
|
||||
}
|
||||
return new CreateDestinationRequest(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
62
src/main/java/dev/evercatch/model/Destination.java
Normal file
62
src/main/java/dev/evercatch/model/Destination.java
Normal file
@@ -0,0 +1,62 @@
|
||||
package dev.evercatch.model;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A webhook destination registered on Evercatch.
|
||||
*/
|
||||
public class Destination {
|
||||
|
||||
private String id;
|
||||
private String name;
|
||||
private String url;
|
||||
private boolean enabled;
|
||||
private List<String> providers;
|
||||
|
||||
@SerializedName("event_types")
|
||||
private List<String> eventTypes;
|
||||
|
||||
private Map<String, String> headers;
|
||||
|
||||
@SerializedName("created_at")
|
||||
private Date createdAt;
|
||||
|
||||
@SerializedName("updated_at")
|
||||
private Date updatedAt;
|
||||
|
||||
public String getId() { return id; }
|
||||
public void setId(String id) { this.id = id; }
|
||||
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
|
||||
public String getUrl() { return url; }
|
||||
public void setUrl(String url) { this.url = url; }
|
||||
|
||||
public boolean isEnabled() { return enabled; }
|
||||
public void setEnabled(boolean enabled) { this.enabled = enabled; }
|
||||
|
||||
public List<String> getProviders() { return providers; }
|
||||
public void setProviders(List<String> providers) { this.providers = providers; }
|
||||
|
||||
public List<String> getEventTypes() { return eventTypes; }
|
||||
public void setEventTypes(List<String> eventTypes) { this.eventTypes = eventTypes; }
|
||||
|
||||
public Map<String, String> getHeaders() { return headers; }
|
||||
public void setHeaders(Map<String, String> headers) { this.headers = headers; }
|
||||
|
||||
public Date getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(Date createdAt) { this.createdAt = createdAt; }
|
||||
|
||||
public Date getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(Date updatedAt) { this.updatedAt = updatedAt; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Destination{id='" + id + "', name='" + name + "', url='" + url + "', enabled=" + enabled + "}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package dev.evercatch.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** Wrapper for paginated destination list responses from the API. */
|
||||
public class DestinationListResponse {
|
||||
|
||||
private List<Destination> data;
|
||||
private String nextCursor;
|
||||
private Integer total;
|
||||
|
||||
public List<Destination> getData() { return data; }
|
||||
public void setData(List<Destination> data) { this.data = data; }
|
||||
|
||||
public String getNextCursor() { return nextCursor; }
|
||||
public void setNextCursor(String nextCursor) { this.nextCursor = nextCursor; }
|
||||
|
||||
public Integer getTotal() { return total; }
|
||||
public void setTotal(Integer total) { this.total = total; }
|
||||
}
|
||||
70
src/main/java/dev/evercatch/model/Event.java
Normal file
70
src/main/java/dev/evercatch/model/Event.java
Normal file
@@ -0,0 +1,70 @@
|
||||
package dev.evercatch.model;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* A webhook event captured by Evercatch.
|
||||
*/
|
||||
public class Event {
|
||||
|
||||
private String id;
|
||||
private String provider;
|
||||
|
||||
@SerializedName("event_type")
|
||||
private String eventType;
|
||||
|
||||
private String status;
|
||||
private JsonObject payload;
|
||||
private JsonObject headers;
|
||||
|
||||
@SerializedName("destination_id")
|
||||
private String destinationId;
|
||||
|
||||
@SerializedName("retry_count")
|
||||
private int retryCount;
|
||||
|
||||
@SerializedName("created_at")
|
||||
private Date createdAt;
|
||||
|
||||
@SerializedName("processed_at")
|
||||
private Date processedAt;
|
||||
|
||||
public String getId() { return id; }
|
||||
public void setId(String id) { this.id = id; }
|
||||
|
||||
public String getProvider() { return provider; }
|
||||
public void setProvider(String provider) { this.provider = provider; }
|
||||
|
||||
public String getEventType() { return eventType; }
|
||||
public void setEventType(String eventType) { this.eventType = eventType; }
|
||||
|
||||
public String getStatus() { return status; }
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
|
||||
public JsonObject getPayload() { return payload; }
|
||||
public void setPayload(JsonObject payload) { this.payload = payload; }
|
||||
|
||||
public JsonObject getHeaders() { return headers; }
|
||||
public void setHeaders(JsonObject headers) { this.headers = headers; }
|
||||
|
||||
public String getDestinationId() { return destinationId; }
|
||||
public void setDestinationId(String destinationId) { this.destinationId = destinationId; }
|
||||
|
||||
public int getRetryCount() { return retryCount; }
|
||||
public void setRetryCount(int retryCount) { this.retryCount = retryCount; }
|
||||
|
||||
public Date getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(Date createdAt) { this.createdAt = createdAt; }
|
||||
|
||||
public Date getProcessedAt() { return processedAt; }
|
||||
public void setProcessedAt(Date processedAt) { this.processedAt = processedAt; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Event{id='" + id + "', provider='" + provider + "', eventType='" + eventType
|
||||
+ "', status='" + status + "'}";
|
||||
}
|
||||
}
|
||||
20
src/main/java/dev/evercatch/model/EventListResponse.java
Normal file
20
src/main/java/dev/evercatch/model/EventListResponse.java
Normal file
@@ -0,0 +1,20 @@
|
||||
package dev.evercatch.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** Wrapper for paginated event list responses from the API. */
|
||||
public class EventListResponse {
|
||||
|
||||
private List<Event> data;
|
||||
private String nextCursor;
|
||||
private Integer total;
|
||||
|
||||
public List<Event> getData() { return data; }
|
||||
public void setData(List<Event> data) { this.data = data; }
|
||||
|
||||
public String getNextCursor() { return nextCursor; }
|
||||
public void setNextCursor(String nextCursor) { this.nextCursor = nextCursor; }
|
||||
|
||||
public Integer getTotal() { return total; }
|
||||
public void setTotal(Integer total) { this.total = total; }
|
||||
}
|
||||
81
src/main/java/dev/evercatch/model/ListEventsRequest.java
Normal file
81
src/main/java/dev/evercatch/model/ListEventsRequest.java
Normal file
@@ -0,0 +1,81 @@
|
||||
package dev.evercatch.model;
|
||||
|
||||
/**
|
||||
* Parameters for filtering and paginating the events list.
|
||||
*
|
||||
* <pre>{@code
|
||||
* ListEventsRequest req = ListEventsRequest.builder()
|
||||
* .provider("stripe")
|
||||
* .eventType("payment.succeeded")
|
||||
* .limit(50)
|
||||
* .build();
|
||||
* }</pre>
|
||||
*/
|
||||
public class ListEventsRequest {
|
||||
|
||||
private final String provider;
|
||||
private final String eventType;
|
||||
private final String status;
|
||||
private final Integer limit;
|
||||
private final String cursor;
|
||||
|
||||
private ListEventsRequest(Builder builder) {
|
||||
this.provider = builder.provider;
|
||||
this.eventType = builder.eventType;
|
||||
this.status = builder.status;
|
||||
this.limit = builder.limit;
|
||||
this.cursor = builder.cursor;
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public String getProvider() { return provider; }
|
||||
public String getEventType() { return eventType; }
|
||||
public String getStatus() { return status; }
|
||||
public Integer getLimit() { return limit; }
|
||||
public String getCursor() { return cursor; }
|
||||
|
||||
public static class Builder {
|
||||
private String provider;
|
||||
private String eventType;
|
||||
private String status;
|
||||
private Integer limit;
|
||||
private String cursor;
|
||||
|
||||
/** Filter by provider slug, e.g. {@code "stripe"}. */
|
||||
public Builder provider(String provider) {
|
||||
this.provider = provider;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Filter by event type, e.g. {@code "payment.succeeded"}. */
|
||||
public Builder eventType(String eventType) {
|
||||
this.eventType = eventType;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Filter by delivery status: {@code "delivered"}, {@code "failed"}, {@code "pending"}. */
|
||||
public Builder status(String status) {
|
||||
this.status = status;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Maximum number of results to return (default 20, max 100). */
|
||||
public Builder limit(int limit) {
|
||||
this.limit = limit;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Pagination cursor from the previous response's {@code nextCursor} field. */
|
||||
public Builder cursor(String cursor) {
|
||||
this.cursor = cursor;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ListEventsRequest build() {
|
||||
return new ListEventsRequest(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
59
src/main/java/dev/evercatch/model/Usage.java
Normal file
59
src/main/java/dev/evercatch/model/Usage.java
Normal file
@@ -0,0 +1,59 @@
|
||||
package dev.evercatch.model;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Account usage statistics returned by {@code /account/usage}.
|
||||
*/
|
||||
public class Usage {
|
||||
|
||||
@SerializedName("events_this_month")
|
||||
private int eventsThisMonth;
|
||||
|
||||
@SerializedName("events_limit")
|
||||
private int eventsLimit;
|
||||
|
||||
@SerializedName("destinations_count")
|
||||
private int destinationsCount;
|
||||
|
||||
@SerializedName("destinations_limit")
|
||||
private int destinationsLimit;
|
||||
|
||||
@SerializedName("plan")
|
||||
private String plan;
|
||||
|
||||
@SerializedName("period_start")
|
||||
private Date periodStart;
|
||||
|
||||
@SerializedName("period_end")
|
||||
private Date periodEnd;
|
||||
|
||||
public int getEventsThisMonth() { return eventsThisMonth; }
|
||||
public void setEventsThisMonth(int eventsThisMonth) { this.eventsThisMonth = eventsThisMonth; }
|
||||
|
||||
public int getEventsLimit() { return eventsLimit; }
|
||||
public void setEventsLimit(int eventsLimit) { this.eventsLimit = eventsLimit; }
|
||||
|
||||
public int getDestinationsCount() { return destinationsCount; }
|
||||
public void setDestinationsCount(int destinationsCount) { this.destinationsCount = destinationsCount; }
|
||||
|
||||
public int getDestinationsLimit() { return destinationsLimit; }
|
||||
public void setDestinationsLimit(int destinationsLimit) { this.destinationsLimit = destinationsLimit; }
|
||||
|
||||
public String getPlan() { return plan; }
|
||||
public void setPlan(String plan) { this.plan = plan; }
|
||||
|
||||
public Date getPeriodStart() { return periodStart; }
|
||||
public void setPeriodStart(Date periodStart) { this.periodStart = periodStart; }
|
||||
|
||||
public Date getPeriodEnd() { return periodEnd; }
|
||||
public void setPeriodEnd(Date periodEnd) { this.periodEnd = periodEnd; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Usage{plan='" + plan + "', eventsThisMonth=" + eventsThisMonth
|
||||
+ "/" + eventsLimit + ", destinations=" + destinationsCount + "/" + destinationsLimit + "}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package dev.evercatch.spring;
|
||||
|
||||
import dev.evercatch.EvercatchClient;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
/**
|
||||
* Spring Boot auto-configuration for the Evercatch SDK.
|
||||
*
|
||||
* <p>A fully configured {@link EvercatchClient} bean is registered automatically when
|
||||
* {@code evercatch.api-key} is present in the application's environment. Override the
|
||||
* bean by declaring your own {@code @Bean} of type {@link EvercatchClient}.</p>
|
||||
*
|
||||
* <p>Example {@code application.properties}:</p>
|
||||
* <pre>
|
||||
* evercatch.api-key=ec_live_abc123
|
||||
* </pre>
|
||||
*
|
||||
* <p>Then inject the client normally:</p>
|
||||
* <pre>{@code
|
||||
* @Service
|
||||
* public class WebhookService {
|
||||
*
|
||||
* private final EvercatchClient evercatch;
|
||||
*
|
||||
* public WebhookService(EvercatchClient evercatch) {
|
||||
* this.evercatch = evercatch;
|
||||
* }
|
||||
*
|
||||
* public void handleEvent(String eventId) throws EvercatchException {
|
||||
* Event event = evercatch.getEvent(eventId);
|
||||
* // ...
|
||||
* }
|
||||
* }
|
||||
* }</pre>
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@ConditionalOnClass(EvercatchClient.class)
|
||||
@ConditionalOnProperty(prefix = "evercatch", name = "api-key")
|
||||
@EnableConfigurationProperties(EvercatchProperties.class)
|
||||
public class EvercatchAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public EvercatchClient evercatchClient(EvercatchProperties properties) {
|
||||
if (properties.getBaseUrl() != null && !properties.getBaseUrl().isBlank()) {
|
||||
return new EvercatchClient(properties.getApiKey(), properties.getBaseUrl());
|
||||
}
|
||||
return new EvercatchClient(properties.getApiKey());
|
||||
}
|
||||
}
|
||||
35
src/main/java/dev/evercatch/spring/EvercatchProperties.java
Normal file
35
src/main/java/dev/evercatch/spring/EvercatchProperties.java
Normal file
@@ -0,0 +1,35 @@
|
||||
package dev.evercatch.spring;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* Spring Boot configuration properties for the Evercatch SDK.
|
||||
*
|
||||
* <p>Set in {@code application.properties} or {@code application.yml}:</p>
|
||||
* <pre>
|
||||
* # application.properties
|
||||
* evercatch.api-key=ec_live_abc123
|
||||
* evercatch.base-url=https://api.evercatch.dev/v1 # optional
|
||||
* </pre>
|
||||
* <pre>
|
||||
* # application.yml
|
||||
* evercatch:
|
||||
* api-key: ec_live_abc123
|
||||
* base-url: https://api.evercatch.dev/v1 # optional
|
||||
* </pre>
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "evercatch")
|
||||
public class EvercatchProperties {
|
||||
|
||||
/** Your Evercatch API key (starts with {@code ec_live_} or {@code ec_test_}). */
|
||||
private String apiKey;
|
||||
|
||||
/** Override the default API base URL. Useful for self-hosted instances. */
|
||||
private String baseUrl;
|
||||
|
||||
public String getApiKey() { return apiKey; }
|
||||
public void setApiKey(String apiKey) { this.apiKey = apiKey; }
|
||||
|
||||
public String getBaseUrl() { return baseUrl; }
|
||||
public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; }
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
dev.evercatch.spring.EvercatchAutoConfiguration
|
||||
243
src/test/java/dev/evercatch/EvercatchClientTest.java
Normal file
243
src/test/java/dev/evercatch/EvercatchClientTest.java
Normal file
@@ -0,0 +1,243 @@
|
||||
package dev.evercatch;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import dev.evercatch.model.*;
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
import okhttp3.mockwebserver.RecordedRequest;
|
||||
import org.junit.jupiter.api.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class EvercatchClientTest {
|
||||
|
||||
private MockWebServer server;
|
||||
private EvercatchClient client;
|
||||
private final Gson gson = new Gson();
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
server = new MockWebServer();
|
||||
server.start();
|
||||
client = new EvercatchClient("ec_test_key", server.url("/").toString());
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() throws IOException {
|
||||
server.shutdown();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor validation
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void constructor_throwsOnNullApiKey() {
|
||||
assertThrows(IllegalArgumentException.class, () -> new EvercatchClient(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructor_throwsOnEmptyApiKey() {
|
||||
assertThrows(IllegalArgumentException.class, () -> new EvercatchClient(" "));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Destinations
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void createDestination_sendsCorrectRequest() throws Exception {
|
||||
Destination dest = new Destination();
|
||||
dest.setId("dst_001");
|
||||
dest.setName("Production");
|
||||
dest.setUrl("https://myapp.com/webhooks");
|
||||
dest.setEnabled(true);
|
||||
|
||||
server.enqueue(new MockResponse()
|
||||
.setResponseCode(201)
|
||||
.setHeader("Content-Type", "application/json")
|
||||
.setBody(gson.toJson(dest)));
|
||||
|
||||
CreateDestinationRequest req = CreateDestinationRequest.builder()
|
||||
.name("Production")
|
||||
.url("https://myapp.com/webhooks")
|
||||
.providers(List.of("stripe"))
|
||||
.build();
|
||||
|
||||
Destination result = client.createDestination(req);
|
||||
|
||||
assertEquals("dst_001", result.getId());
|
||||
assertEquals("Production", result.getName());
|
||||
|
||||
RecordedRequest recorded = server.takeRequest();
|
||||
assertEquals("POST", recorded.getMethod());
|
||||
assertTrue(recorded.getPath().contains("/destinations"));
|
||||
assertEquals("ec_test_key", recorded.getHeader("X-API-Key"));
|
||||
assertNotNull(recorded.getHeader("User-Agent"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createDestination_throwsOnErrorResponse() {
|
||||
server.enqueue(new MockResponse().setResponseCode(400).setBody("{\"error\":\"bad request\"}"));
|
||||
|
||||
CreateDestinationRequest req = CreateDestinationRequest.builder()
|
||||
.name("Test")
|
||||
.url("https://example.com")
|
||||
.providers(List.of("stripe"))
|
||||
.build();
|
||||
|
||||
EvercatchException ex = assertThrows(EvercatchException.class,
|
||||
() -> client.createDestination(req));
|
||||
assertEquals(400, ex.getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void listDestinations_returnsDestinations() throws Exception {
|
||||
Destination d1 = new Destination(); d1.setId("dst_001");
|
||||
Destination d2 = new Destination(); d2.setId("dst_002");
|
||||
|
||||
DestinationListResponse body = new DestinationListResponse();
|
||||
body.setData(List.of(d1, d2));
|
||||
|
||||
server.enqueue(new MockResponse()
|
||||
.setResponseCode(200)
|
||||
.setHeader("Content-Type", "application/json")
|
||||
.setBody(gson.toJson(body)));
|
||||
|
||||
List<Destination> results = client.listDestinations();
|
||||
|
||||
assertEquals(2, results.size());
|
||||
assertEquals("dst_001", results.get(0).getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getDestination_returnsDestination() throws Exception {
|
||||
Destination dest = new Destination(); dest.setId("dst_001");
|
||||
|
||||
server.enqueue(new MockResponse()
|
||||
.setResponseCode(200)
|
||||
.setHeader("Content-Type", "application/json")
|
||||
.setBody(gson.toJson(dest)));
|
||||
|
||||
Destination result = client.getDestination("dst_001");
|
||||
assertEquals("dst_001", result.getId());
|
||||
|
||||
RecordedRequest recorded = server.takeRequest();
|
||||
assertTrue(recorded.getPath().endsWith("/destinations/dst_001"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteDestination_sends204() throws Exception {
|
||||
server.enqueue(new MockResponse().setResponseCode(204));
|
||||
|
||||
assertDoesNotThrow(() -> client.deleteDestination("dst_001"));
|
||||
|
||||
RecordedRequest recorded = server.takeRequest();
|
||||
assertEquals("DELETE", recorded.getMethod());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Events
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void listEvents_withFilters_buildsCorrectUrl() throws Exception {
|
||||
EventListResponse body = new EventListResponse();
|
||||
body.setData(List.of());
|
||||
|
||||
server.enqueue(new MockResponse()
|
||||
.setResponseCode(200)
|
||||
.setHeader("Content-Type", "application/json")
|
||||
.setBody(gson.toJson(body)));
|
||||
|
||||
client.listEvents(ListEventsRequest.builder()
|
||||
.provider("stripe")
|
||||
.limit(25)
|
||||
.build());
|
||||
|
||||
RecordedRequest recorded = server.takeRequest();
|
||||
String path = recorded.getPath();
|
||||
assertTrue(path.contains("provider=stripe"));
|
||||
assertTrue(path.contains("limit=25"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getEvent_returnsEvent() throws Exception {
|
||||
Event event = new Event(); event.setId("evt_001"); event.setProvider("stripe");
|
||||
|
||||
server.enqueue(new MockResponse()
|
||||
.setResponseCode(200)
|
||||
.setHeader("Content-Type", "application/json")
|
||||
.setBody(gson.toJson(event)));
|
||||
|
||||
Event result = client.getEvent("evt_001");
|
||||
assertEquals("evt_001", result.getId());
|
||||
assertEquals("stripe", result.getProvider());
|
||||
}
|
||||
|
||||
@Test
|
||||
void replayEvent_throwsOnPaymentRequired() {
|
||||
server.enqueue(new MockResponse().setResponseCode(402));
|
||||
|
||||
EvercatchException ex = assertThrows(EvercatchException.class,
|
||||
() -> client.replayEvent("evt_001", List.of("dst_001")));
|
||||
|
||||
assertEquals(402, ex.getStatusCode());
|
||||
assertTrue(ex.getMessage().contains("Studio"));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Account / Usage
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void getUsage_returnsUsage() throws Exception {
|
||||
Usage usage = new Usage();
|
||||
usage.setEventsThisMonth(1500);
|
||||
usage.setEventsLimit(10000);
|
||||
usage.setPlan("studio");
|
||||
|
||||
server.enqueue(new MockResponse()
|
||||
.setResponseCode(200)
|
||||
.setHeader("Content-Type", "application/json")
|
||||
.setBody(gson.toJson(usage)));
|
||||
|
||||
Usage result = client.getUsage();
|
||||
assertEquals(1500, result.getEventsThisMonth());
|
||||
assertEquals("studio", result.getPlan());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Builder validation
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void createDestinationRequest_requiresName() {
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
CreateDestinationRequest.builder()
|
||||
.url("https://example.com")
|
||||
.providers(List.of("stripe"))
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createDestinationRequest_requiresUrl() {
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
CreateDestinationRequest.builder()
|
||||
.name("Test")
|
||||
.providers(List.of("stripe"))
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createDestinationRequest_requiresProviders() {
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
CreateDestinationRequest.builder()
|
||||
.name("Test")
|
||||
.url("https://example.com")
|
||||
.build());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user