EC-21: FEAT: Added new Java package and initialized the repo #1
13
.gitignore
vendored
13
.gitignore
vendored
@@ -3,6 +3,19 @@
|
|||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.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
|
# Python
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.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
|
## Tech Stack
|
||||||
|
|
||||||
Describe what this service/module is responsible for within the Evercatch platform.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛠️ Tech Stack
|
|
||||||
|
|
||||||
| Layer | Technology |
|
| Layer | Technology |
|
||||||
| :--- | :--- |
|
| :--- | :--- |
|
||||||
| Language | — |
|
| Language | Java 11+ |
|
||||||
| Framework | — |
|
| HTTP client | OkHttp 4 |
|
||||||
| Key Dependencies | — |
|
| JSON | Gson |
|
||||||
|
| Build | Maven / Gradle |
|
||||||
|
| Spring Boot | Auto-configuration (optional) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚀 Getting Started
|
## Installation
|
||||||
|
|
||||||
### Prerequisites
|
### Maven
|
||||||
|
|
||||||
- Docker & Docker Compose
|
```xml
|
||||||
- Node.js / Python (specify version)
|
<dependency>
|
||||||
|
<groupId>dev.evercatch</groupId>
|
||||||
|
<artifactId>evercatch-java</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
</dependency>
|
||||||
|
```
|
||||||
|
|
||||||
### Local Development
|
### Gradle
|
||||||
|
|
||||||
```bash
|
```gradle
|
||||||
# Clone the repo
|
implementation 'dev.evercatch:evercatch-java:1.0.0'
|
||||||
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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🌿 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.
|
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.
|
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.**
|
**Copyright © 2026 Evercatch.**
|
||||||
Proprietary and confidential. Unauthorised distribution is strictly prohibited.
|
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