diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 41f7de963..fe3ba3317 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -14,7 +14,7 @@ on:
jobs:
lint:
- timeout-minutes: 10
+ timeout-minutes: 15
name: lint
runs-on: ${{ github.repository == 'stainless-sdks/lithic-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
@@ -37,7 +37,7 @@ jobs:
- name: Run lints
run: ./scripts/lint
test:
- timeout-minutes: 10
+ timeout-minutes: 15
name: test
runs-on: ${{ github.repository == 'stainless-sdks/lithic-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index a81246476..a579a4349 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.98.0"
+ ".": "0.99.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 990677dc4..df54f798b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,25 @@
# Changelog
+## 0.99.0 (2025-07-31)
+
+Full Changelog: [v0.98.0...v0.99.0](https://github.com/lithic-com/lithic-java/compare/v0.98.0...v0.99.0)
+
+### Features
+
+* add retryable exception ([9d0e333](https://github.com/lithic-com/lithic-java/commit/9d0e333fa86f6ca6feafd1ea77663082212d4fbd))
+* **client:** ensure compat with proguard ([0cf8803](https://github.com/lithic-com/lithic-java/commit/0cf88037c4534479827ab86f6cfc4e01aa6c9f5e))
+
+
+### Bug Fixes
+
+* **client:** r8 support ([3f77652](https://github.com/lithic-com/lithic-java/commit/3f776524d6c643f14ce1e4a29d5a61919028d27e))
+
+
+### Chores
+
+* **internal:** bump ci test timeout ([dc67b43](https://github.com/lithic-com/lithic-java/commit/dc67b43b859593e8a7a302e2d74935fa45e11584))
+* **internal:** reduce proguard ci logging ([7af8774](https://github.com/lithic-com/lithic-java/commit/7af8774633df28cc77bf86cf3bde7073bc1caf04))
+
## 0.98.0 (2025-07-28)
Full Changelog: [v0.97.1...v0.98.0](https://github.com/lithic-com/lithic-java/compare/v0.97.1...v0.98.0)
diff --git a/README.md b/README.md
index d8a1548f7..247ce2123 100644
--- a/README.md
+++ b/README.md
@@ -2,8 +2,8 @@
-[](https://central.sonatype.com/artifact/com.lithic.api/lithic-java/0.98.0)
-[](https://javadoc.io/doc/com.lithic.api/lithic-java/0.98.0)
+[](https://central.sonatype.com/artifact/com.lithic.api/lithic-java/0.99.0)
+[](https://javadoc.io/doc/com.lithic.api/lithic-java/0.99.0)
@@ -13,7 +13,7 @@ The Lithic Java SDK is similar to the Lithic Kotlin SDK but with minor differenc
-The REST API documentation can be found on [docs.lithic.com](https://docs.lithic.com). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.lithic.api/lithic-java/0.98.0).
+The REST API documentation can be found on [docs.lithic.com](https://docs.lithic.com). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.lithic.api/lithic-java/0.99.0).
@@ -24,7 +24,7 @@ The REST API documentation can be found on [docs.lithic.com](https://docs.lithic
### Gradle
```kotlin
-implementation("com.lithic.api:lithic-java:0.98.0")
+implementation("com.lithic.api:lithic-java:0.99.0")
```
### Maven
@@ -33,7 +33,7 @@ implementation("com.lithic.api:lithic-java:0.98.0")
com.lithic.api
lithic-java
- 0.98.0
+ 0.99.0
```
@@ -232,6 +232,8 @@ The SDK throws custom unchecked exception types:
- [`LithicIoException`](lithic-java-core/src/main/kotlin/com/lithic/api/errors/LithicIoException.kt): I/O networking errors.
+- [`LithicRetryableException`](lithic-java-core/src/main/kotlin/com/lithic/api/errors/LithicRetryableException.kt): Generic error indicating a failure that could be retried by the client.
+
- [`LithicInvalidDataException`](lithic-java-core/src/main/kotlin/com/lithic/api/errors/LithicInvalidDataException.kt): Failure to interpret successfully parsed data. For example, when accessing a property that's supposed to be required, but the API unexpectedly omitted it from the response.
- [`LithicException`](lithic-java-core/src/main/kotlin/com/lithic/api/errors/LithicException.kt): Base class for all exceptions. Most errors will result in one of the previously mentioned ones, but completely generic errors may be thrown using the base class.
@@ -362,6 +364,12 @@ both of which will raise an error if the signature is invalid.
Note that the "body" parameter must be the raw JSON string sent from the server (do not parse it first).
The `.unwrap()` method can parse this JSON for you.
+## ProGuard and R8
+
+Although the SDK uses reflection, it is still usable with [ProGuard](https://github.com/Guardsquare/proguard) and [R8](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization) because `lithic-java-core` is published with a [configuration file](lithic-java-core/src/main/resources/META-INF/proguard/lithic-java-core.pro) containing [keep rules](https://www.guardsquare.com/manual/configuration/usage).
+
+ProGuard and R8 should automatically detect and use the published rules, but you can also manually copy the keep rules if necessary.
+
## Jackson
The SDK depends on [Jackson](https://github.com/FasterXML/jackson) for JSON serialization/deserialization. It is compatible with version 2.13.4 or higher, but depends on version 2.18.2 by default.
diff --git a/build.gradle.kts b/build.gradle.kts
index e99ae9d22..5439db507 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -8,7 +8,7 @@ repositories {
allprojects {
group = "com.lithic.api"
- version = "0.98.0" // x-release-please-version
+ version = "0.99.0" // x-release-please-version
}
subprojects {
diff --git a/lithic-java-core/src/main/kotlin/com/lithic/api/core/http/RetryingHttpClient.kt b/lithic-java-core/src/main/kotlin/com/lithic/api/core/http/RetryingHttpClient.kt
index f267f4786..b27f1442a 100644
--- a/lithic-java-core/src/main/kotlin/com/lithic/api/core/http/RetryingHttpClient.kt
+++ b/lithic-java-core/src/main/kotlin/com/lithic/api/core/http/RetryingHttpClient.kt
@@ -3,6 +3,7 @@ package com.lithic.api.core.http
import com.lithic.api.core.RequestOptions
import com.lithic.api.core.checkRequired
import com.lithic.api.errors.LithicIoException
+import com.lithic.api.errors.LithicRetryableException
import java.io.IOException
import java.time.Clock
import java.time.Duration
@@ -176,9 +177,10 @@ private constructor(
}
private fun shouldRetry(throwable: Throwable): Boolean =
- // Only retry IOException and LithicIoException, other exceptions are not intended to be
- // retried.
- throwable is IOException || throwable is LithicIoException
+ // Only retry known retryable exceptions, other exceptions are not intended to be retried.
+ throwable is IOException ||
+ throwable is LithicIoException ||
+ throwable is LithicRetryableException
private fun getRetryBackoffDuration(retries: Int, response: HttpResponse?): Duration {
// About the Retry-After header:
diff --git a/lithic-java-core/src/main/kotlin/com/lithic/api/errors/LithicRetryableException.kt b/lithic-java-core/src/main/kotlin/com/lithic/api/errors/LithicRetryableException.kt
new file mode 100644
index 000000000..45f054257
--- /dev/null
+++ b/lithic-java-core/src/main/kotlin/com/lithic/api/errors/LithicRetryableException.kt
@@ -0,0 +1,14 @@
+package com.lithic.api.errors
+
+/**
+ * Exception that indicates a transient error that can be retried.
+ *
+ * When this exception is thrown during an HTTP request, the SDK will automatically retry the
+ * request up to the maximum number of retries.
+ *
+ * @param message A descriptive error message
+ * @param cause The underlying cause of this exception, if any
+ */
+class LithicRetryableException
+@JvmOverloads
+constructor(message: String? = null, cause: Throwable? = null) : LithicException(message, cause)
diff --git a/lithic-java-core/src/main/resources/META-INF/proguard/lithic-java-core.pro b/lithic-java-core/src/main/resources/META-INF/proguard/lithic-java-core.pro
new file mode 100644
index 000000000..c8d9e044d
--- /dev/null
+++ b/lithic-java-core/src/main/resources/META-INF/proguard/lithic-java-core.pro
@@ -0,0 +1,32 @@
+# Jackson uses reflection and depends heavily on runtime attributes.
+-keepattributes Exceptions,InnerClasses,Signature,Deprecated,*Annotation*
+
+# Jackson uses Kotlin reflection utilities, which themselves use reflection to access things.
+-keep class kotlin.reflect.** { *; }
+-keep class kotlin.Metadata { *; }
+
+# Jackson uses reflection to access enum members (e.g. via `java.lang.Class.getEnumConstants()`).
+-keepclassmembers class com.fasterxml.jackson.** extends java.lang.Enum {
+ ;
+ public static **[] values();
+ public static ** valueOf(java.lang.String);
+}
+
+# Jackson uses reflection to access annotation members.
+-keepclassmembers @interface com.fasterxml.jackson.annotation.** {
+ *;
+}
+
+# Jackson uses reified type information to serialize and deserialize our classes (via `TypeReference`).
+-keep class com.fasterxml.jackson.core.type.TypeReference { *; }
+-keep class * extends com.fasterxml.jackson.core.type.TypeReference { *; }
+
+# Jackson uses reflection to access our class serializers and deserializers.
+-keep @com.fasterxml.jackson.databind.annotation.JsonSerialize class com.lithic.api.** { *; }
+-keep @com.fasterxml.jackson.databind.annotation.JsonDeserialize class com.lithic.api.** { *; }
+
+# Jackson uses reflection to serialize and deserialize our classes based on their constructors and annotated members.
+-keepclassmembers class com.lithic.api.** {
+ (...);
+ @com.fasterxml.jackson.annotation.* *;
+}
\ No newline at end of file
diff --git a/lithic-java-core/src/test/kotlin/com/lithic/api/core/http/RetryingHttpClientTest.kt b/lithic-java-core/src/test/kotlin/com/lithic/api/core/http/RetryingHttpClientTest.kt
index 9745e4daf..a58f57c34 100644
--- a/lithic-java-core/src/test/kotlin/com/lithic/api/core/http/RetryingHttpClientTest.kt
+++ b/lithic-java-core/src/test/kotlin/com/lithic/api/core/http/RetryingHttpClientTest.kt
@@ -6,6 +6,7 @@ import com.github.tomakehurst.wiremock.junit5.WireMockTest
import com.github.tomakehurst.wiremock.stubbing.Scenario
import com.lithic.api.client.okhttp.OkHttpClient
import com.lithic.api.core.RequestOptions
+import com.lithic.api.errors.LithicRetryableException
import java.io.InputStream
import java.time.Duration
import java.util.concurrent.CompletableFuture
@@ -251,6 +252,82 @@ internal class RetryingHttpClientTest {
assertNoResponseLeaks()
}
+ @ParameterizedTest
+ @ValueSource(booleans = [false, true])
+ fun execute_withRetryableException(async: Boolean) {
+ stubFor(post(urlPathEqualTo("/something")).willReturn(ok()))
+
+ var callCount = 0
+ val failingHttpClient =
+ object : HttpClient {
+ override fun execute(
+ request: HttpRequest,
+ requestOptions: RequestOptions,
+ ): HttpResponse {
+ callCount++
+ if (callCount == 1) {
+ throw LithicRetryableException("Simulated retryable failure")
+ }
+ return httpClient.execute(request, requestOptions)
+ }
+
+ override fun executeAsync(
+ request: HttpRequest,
+ requestOptions: RequestOptions,
+ ): CompletableFuture {
+ callCount++
+ if (callCount == 1) {
+ val future = CompletableFuture()
+ future.completeExceptionally(
+ LithicRetryableException("Simulated retryable failure")
+ )
+ return future
+ }
+ return httpClient.executeAsync(request, requestOptions)
+ }
+
+ override fun close() = httpClient.close()
+ }
+
+ val retryingClient =
+ RetryingHttpClient.builder()
+ .httpClient(failingHttpClient)
+ .maxRetries(2)
+ .sleeper(
+ object : RetryingHttpClient.Sleeper {
+
+ override fun sleep(duration: Duration) {}
+
+ override fun sleepAsync(duration: Duration): CompletableFuture =
+ CompletableFuture.completedFuture(null)
+ }
+ )
+ .build()
+
+ val response =
+ retryingClient.execute(
+ HttpRequest.builder()
+ .method(HttpMethod.POST)
+ .baseUrl(baseUrl)
+ .addPathSegment("something")
+ .build(),
+ async,
+ )
+
+ assertThat(response.statusCode()).isEqualTo(200)
+ verify(
+ 1,
+ postRequestedFor(urlPathEqualTo("/something"))
+ .withHeader("x-stainless-retry-count", equalTo("1")),
+ )
+ verify(
+ 0,
+ postRequestedFor(urlPathEqualTo("/something"))
+ .withHeader("x-stainless-retry-count", equalTo("0")),
+ )
+ assertNoResponseLeaks()
+ }
+
private fun retryingHttpClientBuilder() =
RetryingHttpClient.builder()
.httpClient(httpClient)
diff --git a/lithic-java-proguard-test/build.gradle.kts b/lithic-java-proguard-test/build.gradle.kts
new file mode 100644
index 000000000..06512330c
--- /dev/null
+++ b/lithic-java-proguard-test/build.gradle.kts
@@ -0,0 +1,103 @@
+plugins {
+ id("lithic.kotlin")
+ id("com.gradleup.shadow") version "8.3.8"
+}
+
+buildscript {
+ repositories {
+ google()
+ }
+
+ dependencies {
+ classpath("com.guardsquare:proguard-gradle:7.4.2")
+ classpath("com.android.tools:r8:8.3.37")
+ }
+}
+
+dependencies {
+ testImplementation(project(":lithic-java"))
+ testImplementation(kotlin("test"))
+ testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3")
+ testImplementation("org.assertj:assertj-core:3.25.3")
+ testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.4")
+}
+
+tasks.shadowJar {
+ from(sourceSets.test.get().output)
+ configurations = listOf(project.configurations.testRuntimeClasspath.get())
+}
+
+val proguardJarPath = "${layout.buildDirectory.get()}/libs/${project.name}-${project.version}-proguard.jar"
+val proguardJar by tasks.registering(proguard.gradle.ProGuardTask::class) {
+ group = "verification"
+ dependsOn(tasks.shadowJar)
+ notCompatibleWithConfigurationCache("ProGuard")
+
+ injars(tasks.shadowJar)
+ outjars(proguardJarPath)
+ printmapping("${layout.buildDirectory.get()}/proguard-mapping.txt")
+
+ dontwarn()
+
+ val javaHome = System.getProperty("java.home")
+ if (System.getProperty("java.version").startsWith("1.")) {
+ // Before Java 9, the runtime classes were packaged in a single jar file.
+ libraryjars("$javaHome/lib/rt.jar")
+ } else {
+ // As of Java 9, the runtime classes are packaged in modular jmod files.
+ libraryjars(
+ // Filters must be specified first, as a map.
+ mapOf("jarfilter" to "!**.jar", "filter" to "!module-info.class"),
+ "$javaHome/jmods/java.base.jmod"
+ )
+ }
+
+ configuration("./test.pro")
+ configuration("../lithic-java-core/src/main/resources/META-INF/proguard/lithic-java-core.pro")
+}
+
+val testProGuard by tasks.registering(JavaExec::class) {
+ group = "verification"
+ dependsOn(proguardJar)
+ notCompatibleWithConfigurationCache("ProGuard")
+
+ mainClass.set("com.lithic.api.proguard.ProGuardCompatibilityTest")
+ classpath = files(proguardJarPath)
+}
+
+val r8JarPath = "${layout.buildDirectory.get()}/libs/${project.name}-${project.version}-r8.jar"
+val r8Jar by tasks.registering(JavaExec::class) {
+ group = "verification"
+ dependsOn(tasks.shadowJar)
+ notCompatibleWithConfigurationCache("R8")
+
+ mainClass.set("com.android.tools.r8.R8")
+ classpath = buildscript.configurations["classpath"]
+
+ args = listOf(
+ "--release",
+ "--classfile",
+ "--output", r8JarPath,
+ "--lib", System.getProperty("java.home"),
+ "--pg-conf", "./test.pro",
+ "--pg-conf", "../lithic-java-core/src/main/resources/META-INF/proguard/lithic-java-core.pro",
+ "--pg-map-output", "${layout.buildDirectory.get()}/r8-mapping.txt",
+ tasks.shadowJar.get().archiveFile.get().asFile.absolutePath,
+ )
+}
+
+val testR8 by tasks.registering(JavaExec::class) {
+ group = "verification"
+ dependsOn(r8Jar)
+ notCompatibleWithConfigurationCache("R8")
+
+ mainClass.set("com.lithic.api.proguard.ProGuardCompatibilityTest")
+ classpath = files(r8JarPath)
+}
+
+tasks.test {
+ dependsOn(testProGuard)
+ dependsOn(testR8)
+ // We defer to the tests run via the ProGuard JAR.
+ enabled = false
+}
diff --git a/lithic-java-proguard-test/src/test/kotlin/com/lithic/api/proguard/ProGuardCompatibilityTest.kt b/lithic-java-proguard-test/src/test/kotlin/com/lithic/api/proguard/ProGuardCompatibilityTest.kt
new file mode 100644
index 000000000..fed75087f
--- /dev/null
+++ b/lithic-java-proguard-test/src/test/kotlin/com/lithic/api/proguard/ProGuardCompatibilityTest.kt
@@ -0,0 +1,299 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.lithic.api.proguard
+
+import com.fasterxml.jackson.module.kotlin.jacksonTypeRef
+import com.lithic.api.client.okhttp.LithicOkHttpClient
+import com.lithic.api.core.jsonMapper
+import com.lithic.api.models.AccountHolderUpdateResponse
+import com.lithic.api.models.CardSpendLimits
+import com.lithic.api.models.KybBusinessEntity
+import com.lithic.api.models.RequiredDocument
+import com.lithic.api.models.SpendLimitDuration
+import java.time.OffsetDateTime
+import kotlin.reflect.full.memberFunctions
+import kotlin.reflect.jvm.javaMethod
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+
+internal class ProGuardCompatibilityTest {
+
+ companion object {
+
+ @JvmStatic
+ fun main(args: Array) {
+ // To debug that we're using the right JAR.
+ val jarPath = this::class.java.getProtectionDomain().codeSource.location
+ println("JAR being used: $jarPath")
+
+ // We have to manually run the test methods instead of using the JUnit runner because it
+ // seems impossible to get working with R8.
+ val test = ProGuardCompatibilityTest()
+ test::class
+ .memberFunctions
+ .asSequence()
+ .filter { function ->
+ function.javaMethod?.isAnnotationPresent(Test::class.java) == true
+ }
+ .forEach { it.call(test) }
+ }
+ }
+
+ @Test
+ fun proguardRules() {
+ val rulesFile =
+ javaClass.classLoader.getResourceAsStream("META-INF/proguard/lithic-java-core.pro")
+
+ assertThat(rulesFile).isNotNull()
+ }
+
+ @Test
+ fun client() {
+ val client = LithicOkHttpClient.builder().apiKey("My Lithic API Key").build()
+
+ assertThat(client).isNotNull()
+ assertThat(client.accounts()).isNotNull()
+ assertThat(client.accountHolders()).isNotNull()
+ assertThat(client.authRules()).isNotNull()
+ assertThat(client.authStreamEnrollment()).isNotNull()
+ assertThat(client.tokenizationDecisioning()).isNotNull()
+ assertThat(client.tokenizations()).isNotNull()
+ assertThat(client.cards()).isNotNull()
+ assertThat(client.balances()).isNotNull()
+ assertThat(client.aggregateBalances()).isNotNull()
+ assertThat(client.disputes()).isNotNull()
+ assertThat(client.events()).isNotNull()
+ assertThat(client.transfers()).isNotNull()
+ assertThat(client.financialAccounts()).isNotNull()
+ assertThat(client.transactions()).isNotNull()
+ assertThat(client.responderEndpoints()).isNotNull()
+ assertThat(client.externalBankAccounts()).isNotNull()
+ assertThat(client.payments()).isNotNull()
+ assertThat(client.threeDS()).isNotNull()
+ assertThat(client.reports()).isNotNull()
+ assertThat(client.cardPrograms()).isNotNull()
+ assertThat(client.digitalCardArt()).isNotNull()
+ assertThat(client.bookTransfers()).isNotNull()
+ assertThat(client.creditProducts()).isNotNull()
+ assertThat(client.externalPayments()).isNotNull()
+ assertThat(client.managementOperations()).isNotNull()
+ assertThat(client.fundingEvents()).isNotNull()
+ assertThat(client.fraud()).isNotNull()
+ assertThat(client.networkPrograms()).isNotNull()
+ }
+
+ @Test
+ fun cardSpendLimitsRoundtrip() {
+ val jsonMapper = jsonMapper()
+ val cardSpendLimits =
+ CardSpendLimits.builder()
+ .availableSpendLimit(
+ CardSpendLimits.AvailableSpendLimit.builder()
+ .annually(200000L)
+ .forever(300000L)
+ .monthly(200000L)
+ .build()
+ )
+ .spendLimit(
+ CardSpendLimits.SpendLimit.builder()
+ .annually(500000L)
+ .forever(500000L)
+ .monthly(500000L)
+ .build()
+ )
+ .spendVelocity(
+ CardSpendLimits.SpendVelocity.builder()
+ .annually(300000L)
+ .forever(200000L)
+ .monthly(300000L)
+ .build()
+ )
+ .build()
+
+ val roundtrippedCardSpendLimits =
+ jsonMapper.readValue(
+ jsonMapper.writeValueAsString(cardSpendLimits),
+ jacksonTypeRef(),
+ )
+
+ assertThat(roundtrippedCardSpendLimits).isEqualTo(cardSpendLimits)
+ }
+
+ @Test
+ fun accountHolderUpdateResponseRoundtrip() {
+ val jsonMapper = jsonMapper()
+ val accountHolderUpdateResponse =
+ AccountHolderUpdateResponse.ofKybKycPatch(
+ AccountHolderUpdateResponse.KybKycPatchResponse.builder()
+ .token("182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e")
+ .accountToken("182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e")
+ .addBeneficialOwnerEntity(
+ KybBusinessEntity.builder()
+ .address(
+ KybBusinessEntity.Address.builder()
+ .address1("123 Old Forest Way")
+ .city("Omaha")
+ .country("USA")
+ .postalCode("68022")
+ .state("NE")
+ .address2("address2")
+ .build()
+ )
+ .governmentId("114-123-1513")
+ .legalBusinessName("Acme, Inc.")
+ .addPhoneNumber("+15555555555")
+ .dbaBusinessName("dba_business_name")
+ .parentCompany("parent_company")
+ .build()
+ )
+ .addBeneficialOwnerIndividual(
+ AccountHolderUpdateResponse.KybKycPatchResponse.Individual.builder()
+ .address(
+ AccountHolderUpdateResponse.KybKycPatchResponse.Individual.Address
+ .builder()
+ .address1("123 Old Forest Way")
+ .city("Omaha")
+ .country("USA")
+ .postalCode("68022")
+ .state("NE")
+ .address2("address2")
+ .build()
+ )
+ .dob("1991-03-08 08:00:00")
+ .email("tom@middle-earth.com")
+ .firstName("Tom")
+ .governmentId("111-23-1412")
+ .lastName("Bombadil")
+ .phoneNumber("+15555555555")
+ .build()
+ )
+ .businessAccountToken("182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e")
+ .businessEntity(
+ KybBusinessEntity.builder()
+ .address(
+ KybBusinessEntity.Address.builder()
+ .address1("123 Old Forest Way")
+ .city("Omaha")
+ .country("USA")
+ .postalCode("68022")
+ .state("NE")
+ .address2("address2")
+ .build()
+ )
+ .governmentId("114-123-1513")
+ .legalBusinessName("Acme, Inc.")
+ .addPhoneNumber("+15555555555")
+ .dbaBusinessName("dba_business_name")
+ .parentCompany("parent_company")
+ .build()
+ )
+ .controlPerson(
+ AccountHolderUpdateResponse.KybKycPatchResponse.Individual.builder()
+ .address(
+ AccountHolderUpdateResponse.KybKycPatchResponse.Individual.Address
+ .builder()
+ .address1("123 Old Forest Way")
+ .city("Omaha")
+ .country("USA")
+ .postalCode("68022")
+ .state("NE")
+ .address2("address2")
+ .build()
+ )
+ .dob("1991-03-08 08:00:00")
+ .email("tom@middle-earth.com")
+ .firstName("Tom")
+ .governmentId("111-23-1412")
+ .lastName("Bombadil")
+ .phoneNumber("+15555555555")
+ .build()
+ )
+ .created(OffsetDateTime.parse("2019-12-27T18:11:19.117Z"))
+ .email("email")
+ .exemptionType(
+ AccountHolderUpdateResponse.KybKycPatchResponse.ExemptionType
+ .AUTHORIZED_USER
+ )
+ .externalId("external_id")
+ .individual(
+ AccountHolderUpdateResponse.KybKycPatchResponse.Individual.builder()
+ .address(
+ AccountHolderUpdateResponse.KybKycPatchResponse.Individual.Address
+ .builder()
+ .address1("123 Old Forest Way")
+ .city("Omaha")
+ .country("USA")
+ .postalCode("68022")
+ .state("NE")
+ .address2("address2")
+ .build()
+ )
+ .dob("1991-03-08 08:00:00")
+ .email("tom@middle-earth.com")
+ .firstName("Tom")
+ .governmentId("111-23-1412")
+ .lastName("Bombadil")
+ .phoneNumber("+15555555555")
+ .build()
+ )
+ .natureOfBusiness("nature_of_business")
+ .phoneNumber("phone_number")
+ .addRequiredDocument(
+ RequiredDocument.builder()
+ .entityToken("182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e")
+ .addStatusReason("string")
+ .addValidDocument("string")
+ .build()
+ )
+ .status(AccountHolderUpdateResponse.KybKycPatchResponse.Status.ACCEPTED)
+ .addStatusReason(
+ AccountHolderUpdateResponse.KybKycPatchResponse.StatusReasons
+ .ADDRESS_VERIFICATION_FAILURE
+ )
+ .userType(AccountHolderUpdateResponse.KybKycPatchResponse.UserType.BUSINESS)
+ .verificationApplication(
+ AccountHolderUpdateResponse.KybKycPatchResponse.VerificationApplication
+ .builder()
+ .created(OffsetDateTime.parse("2019-12-27T18:11:19.117Z"))
+ .status(
+ AccountHolderUpdateResponse.KybKycPatchResponse
+ .VerificationApplication
+ .Status
+ .ACCEPTED
+ )
+ .addStatusReason(
+ AccountHolderUpdateResponse.KybKycPatchResponse
+ .VerificationApplication
+ .StatusReasons
+ .ADDRESS_VERIFICATION_FAILURE
+ )
+ .updated(OffsetDateTime.parse("2019-12-27T18:11:19.117Z"))
+ .build()
+ )
+ .websiteUrl("website_url")
+ .build()
+ )
+
+ val roundtrippedAccountHolderUpdateResponse =
+ jsonMapper.readValue(
+ jsonMapper.writeValueAsString(accountHolderUpdateResponse),
+ jacksonTypeRef(),
+ )
+
+ assertThat(roundtrippedAccountHolderUpdateResponse).isEqualTo(accountHolderUpdateResponse)
+ }
+
+ @Test
+ fun spendLimitDurationRoundtrip() {
+ val jsonMapper = jsonMapper()
+ val spendLimitDuration = SpendLimitDuration.ANNUALLY
+
+ val roundtrippedSpendLimitDuration =
+ jsonMapper.readValue(
+ jsonMapper.writeValueAsString(spendLimitDuration),
+ jacksonTypeRef(),
+ )
+
+ assertThat(roundtrippedSpendLimitDuration).isEqualTo(spendLimitDuration)
+ }
+}
diff --git a/lithic-java-proguard-test/test.pro b/lithic-java-proguard-test/test.pro
new file mode 100644
index 000000000..2405d4e26
--- /dev/null
+++ b/lithic-java-proguard-test/test.pro
@@ -0,0 +1,8 @@
+# Specify the entrypoint where ProGuard starts to determine what's reachable.
+-keep class com.lithic.api.proguard.** { *; }
+
+# For the testing framework.
+-keep class org.junit.** { *; }
+
+# Many warnings don't apply for our testing purposes.
+-dontwarn
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 906a2f632..fdce5b627 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -3,4 +3,5 @@ rootProject.name = "lithic-java-root"
include("lithic-java")
include("lithic-java-client-okhttp")
include("lithic-java-core")
+include("lithic-java-proguard-test")
include("lithic-java-example")