() {
- public String apply(String s) {
- return stringStringEntry.getKey() + "=" + s;
- }
- }));
- }
- }));
- }
- return builder.toString();
+
+ String baseUrl = protocol + "://" + host + path;
+
+ return UriComponentsBuilder.fromUriString(baseUrl)
+ .queryParams(queryParamsBuilder.build().getQueryParams())
+ .build()
+ .encode()
+ .toUri();
}
}
diff --git a/src/main/java/de/focus_shift/lexoffice/java/sdk/SearchStringEncoder.java b/src/main/java/de/focus_shift/lexoffice/java/sdk/SearchStringEncoder.java
new file mode 100644
index 0000000..1c5ce69
--- /dev/null
+++ b/src/main/java/de/focus_shift/lexoffice/java/sdk/SearchStringEncoder.java
@@ -0,0 +1,51 @@
+package de.focus_shift.lexoffice.java.sdk;
+
+/**
+ * Utility class for encoding search strings according to Lexoffice API requirements.
+ *
+ * Due to technical constraints, for some endpoints (contact, vouchers, voucherlist),
+ * search strings require special handling: HTML special characters {@code &}, {@code <},
+ * and {@code >} need to be sent in their HTML encoded form before URL encoding is applied.
+ *
+ *
The HTML encoding needs to match the canonical representation as stored in the
+ * Lexware database. Unicode character encoding or any other form of representation
+ * should not be used.
+ *
+ * @see
+ * Lexoffice API - Search String Encoding
+ */
+public final class SearchStringEncoder {
+
+ private SearchStringEncoder() {
+ // utility class
+ }
+
+ /**
+ * HTML encodes special characters in search strings for Lexoffice API endpoints
+ * that require this encoding (contact, vouchers, voucherlist).
+ *
+ *
The following characters are encoded:
+ *
+ * - {@code &} → {@code &}
+ * - {@code <} → {@code <}
+ * - {@code >} → {@code >}
+ *
+ *
+ * Example: {@code "johnson & partner"} becomes {@code "johnson & partner"}
+ *
+ * @param value the search string to encode, may be null
+ * @return the HTML encoded string, or null if input was null
+ * @see
+ * Lexoffice API - Search String Encoding
+ */
+ public static String htmlEncode(String value) {
+ if (value == null) {
+ return null;
+ }
+ // Order matters: & must be encoded first to avoid double-encoding
+ return value
+ .replace("&", "&")
+ .replace("<", "<")
+ .replace(">", ">");
+ }
+}
diff --git a/src/main/java/de/focus_shift/lexoffice/java/sdk/chain/ContactChain.java b/src/main/java/de/focus_shift/lexoffice/java/sdk/chain/ContactChain.java
index 49af491..d88952d 100644
--- a/src/main/java/de/focus_shift/lexoffice/java/sdk/chain/ContactChain.java
+++ b/src/main/java/de/focus_shift/lexoffice/java/sdk/chain/ContactChain.java
@@ -2,6 +2,7 @@
import de.focus_shift.lexoffice.java.sdk.RequestContext;
+import de.focus_shift.lexoffice.java.sdk.SearchStringEncoder;
import de.focus_shift.lexoffice.java.sdk.model.Contact;
import de.focus_shift.lexoffice.java.sdk.model.ItemCreatedResult;
import de.focus_shift.lexoffice.java.sdk.model.Page;
@@ -68,19 +69,23 @@ public Fetch pageSize(int pageSize) {
/**
* filters contacts where any of their email addresses inside the emailAddresses JSON object match the given email value. At least 3 characters are necessary to successfully complete the query.
+ *
+ * @see SearchStringEncoder
*/
public Fetch email(String email) {
super.getUriBuilder()
- .addParameter("email", email);
+ .addParameter("email", SearchStringEncoder.htmlEncode(email));
return this;
}
/**
* filters contacts whose name matches the given name value. At least 3 characters are necessary to successfully complete the query.
+ *
+ * @see SearchStringEncoder
*/
public Fetch name(String name) {
super.getUriBuilder()
- .addParameter("name", name);
+ .addParameter("name", SearchStringEncoder.htmlEncode(name));
return this;
}
diff --git a/src/main/java/de/focus_shift/lexoffice/java/sdk/chain/VoucherListChain.java b/src/main/java/de/focus_shift/lexoffice/java/sdk/chain/VoucherListChain.java
index dee7787..7e4a23a 100644
--- a/src/main/java/de/focus_shift/lexoffice/java/sdk/chain/VoucherListChain.java
+++ b/src/main/java/de/focus_shift/lexoffice/java/sdk/chain/VoucherListChain.java
@@ -3,6 +3,7 @@
import com.google.common.base.Joiner;
import de.focus_shift.lexoffice.java.sdk.LexofficeApi;
import de.focus_shift.lexoffice.java.sdk.RequestContext;
+import de.focus_shift.lexoffice.java.sdk.SearchStringEncoder;
import de.focus_shift.lexoffice.java.sdk.model.Page;
import de.focus_shift.lexoffice.java.sdk.model.Voucher;
import de.focus_shift.lexoffice.java.sdk.model.VoucherStatus;
@@ -91,9 +92,12 @@ public VoucherListChain voucherStatus(VoucherStatus... voucherStatus) {
return this;
}
+ /**
+ * @see SearchStringEncoder
+ */
public VoucherListChain voucherNumber(String voucherNumber) {
super.getUriBuilder()
- .addParameter("voucherNumber", voucherNumber);
+ .addParameter("voucherNumber", SearchStringEncoder.htmlEncode(voucherNumber));
return this;
}
@@ -120,9 +124,12 @@ public VoucherListChain dueDate(Date dueDate) {
return this;
}
+ /**
+ * @see SearchStringEncoder
+ */
public VoucherListChain contactName(String contactName) {
super.getUriBuilder()
- .addParameter("contactName", contactName);
+ .addParameter("contactName", SearchStringEncoder.htmlEncode(contactName));
return this;
}
diff --git a/src/main/java/de/focus_shift/lexoffice/java/sdk/model/ExecutionInterval.java b/src/main/java/de/focus_shift/lexoffice/java/sdk/model/ExecutionInterval.java
index a406c01..adf362a 100644
--- a/src/main/java/de/focus_shift/lexoffice/java/sdk/model/ExecutionInterval.java
+++ b/src/main/java/de/focus_shift/lexoffice/java/sdk/model/ExecutionInterval.java
@@ -6,8 +6,10 @@
public enum ExecutionInterval {
WEEKLY("WEEKLY"),
+ BIWEEKLY("BIWEEKLY"),
MONTHLY("MONTHLY"),
QUARTERLY("QUARTERLY"),
+ BIANNUALLY("BIANNUALLY"),
ANNUALLY("ANNUALLY");
@Getter
diff --git a/src/test/java/de/focus_shift/lexoffice/java/sdk/RestUriBuilderTest.java b/src/test/java/de/focus_shift/lexoffice/java/sdk/RestUriBuilderTest.java
new file mode 100644
index 0000000..6c9515a
--- /dev/null
+++ b/src/test/java/de/focus_shift/lexoffice/java/sdk/RestUriBuilderTest.java
@@ -0,0 +1,257 @@
+package de.focus_shift.lexoffice.java.sdk;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.net.URI;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+class RestUriBuilderTest {
+
+ private static final String LEXOFFICE_API_HOST = "api.lexware.io/v1";
+
+ @Test
+ void buildSimpleUrl() {
+ URI uri = new RestUriBuilder()
+ .protocol("https")
+ .host(LEXOFFICE_API_HOST)
+ .path("/contacts")
+ .build();
+
+ assertThat(uri.toString()).isEqualTo("https://api.lexware.io/v1/contacts");
+ }
+
+ @Test
+ void buildUrlWithSingleQueryParameter() {
+ URI uri = new RestUriBuilder()
+ .protocol("https")
+ .host(LEXOFFICE_API_HOST)
+ .path("/contacts")
+ .addParameter("page", 0)
+ .build();
+
+ assertThat(uri.toString()).isEqualTo("https://api.lexware.io/v1/contacts?page=0");
+ }
+
+ @Test
+ void buildUrlWithMultipleQueryParameters() {
+ URI uri = new RestUriBuilder()
+ .protocol("https")
+ .host(LEXOFFICE_API_HOST)
+ .path("/contacts")
+ .addParameter("page", 0)
+ .addParameter("size", 25)
+ .build();
+
+ assertThat(uri.toString()).isEqualTo("https://api.lexware.io/v1/contacts?page=0&size=25");
+ }
+
+ @Test
+ void buildUrlWithMultipleValuesForSameKey() {
+ URI uri = new RestUriBuilder()
+ .protocol("https")
+ .host(LEXOFFICE_API_HOST)
+ .path("/voucherlist")
+ .addParameters("voucherStatus", List.of("open", "paid"))
+ .build();
+
+ assertThat(uri.toString()).isEqualTo("https://api.lexware.io/v1/voucherlist?voucherStatus=open&voucherStatus=paid");
+ }
+
+ @Test
+ void buildUrlEncodesSpecialCharactersInQueryParameters() {
+ URI uri = new RestUriBuilder()
+ .protocol("https")
+ .host(LEXOFFICE_API_HOST)
+ .path("/contacts")
+ .addParameter("name", "Schmidt & Partner, München")
+ .build();
+
+ assertThat(uri.toString()).isEqualTo("https://api.lexware.io/v1/contacts?name=Schmidt%20%26%20Partner,%20M%C3%BCnchen");
+ }
+
+ @Test
+ void buildUrlEncodesAmpersandInQueryParameters() {
+ URI uri = new RestUriBuilder()
+ .protocol("https")
+ .host(LEXOFFICE_API_HOST)
+ .path("/contacts")
+ .addParameter("name", "Müller & Söhne GmbH")
+ .build();
+
+ assertThat(uri.toString()).contains("name=M%C3%BCller%20%26%20S%C3%B6hne%20GmbH");
+ }
+
+ @Test
+ void buildUrlEncodesEqualsSignInQueryParameters() {
+ URI uri = new RestUriBuilder()
+ .protocol("https")
+ .host(LEXOFFICE_API_HOST)
+ .path("/contacts")
+ .addParameter("filter", "status=active")
+ .build();
+
+ assertThat(uri.toString()).contains("filter=status%3Dactive");
+ }
+
+ @Test
+ void buildUrlEncodesSpacesInQueryParameters() {
+ URI uri = new RestUriBuilder()
+ .protocol("https")
+ .host(LEXOFFICE_API_HOST)
+ .path("/contacts")
+ .addParameter("name", "Berliner Kindl GmbH")
+ .build();
+
+ assertThat(uri.toString()).contains("name=Berliner%20Kindl%20GmbH");
+ }
+
+ @Test
+ void buildUrlEncodesPlusSignInQueryParameters() {
+ URI uri = new RestUriBuilder()
+ .protocol("https")
+ .host(LEXOFFICE_API_HOST)
+ .path("/contacts")
+ .addParameter("name", "Software C++ GmbH")
+ .build();
+
+ // Spring's UriComponentsBuilder keeps + as-is (valid in RFC 3986 query strings)
+ // When decoded, servers should interpret this correctly
+ assertThat(uri.toString()).contains("name=Software%20C++%20GmbH");
+ }
+
+ @Test
+ void buildUrlEncodesHashInQueryParameters() {
+ URI uri = new RestUriBuilder()
+ .protocol("https")
+ .host(LEXOFFICE_API_HOST)
+ .path("/contacts")
+ .addParameter("tag", "#wichtig")
+ .build();
+
+ assertThat(uri.toString()).contains("tag=%23wichtig");
+ }
+
+ @Test
+ void buildUrlWithHttpProtocol() {
+ URI uri = new RestUriBuilder()
+ .protocol("http")
+ .host("localhost:8080/v1")
+ .path("/contacts")
+ .build();
+
+ assertThat(uri.toString()).isEqualTo("http://localhost:8080/v1/contacts");
+ }
+
+ @Test
+ void appendPathConcatenatesCorrectly() {
+ String contactId = "be9475a4-ef80-442b-95eb-e56a7b2d5596";
+
+ URI uri = new RestUriBuilder()
+ .protocol("https")
+ .host(LEXOFFICE_API_HOST)
+ .path("/contacts")
+ .appendPath("/")
+ .appendPath(contactId)
+ .build();
+
+ assertThat(uri.toString()).isEqualTo("https://api.lexware.io/v1/contacts/be9475a4-ef80-442b-95eb-e56a7b2d5596");
+ }
+
+ @Test
+ void buildWithEmptyPathSucceeds() {
+ URI uri = new RestUriBuilder()
+ .protocol("https")
+ .host(LEXOFFICE_API_HOST)
+ .build();
+
+ assertThat(uri.toString()).isEqualTo("https://api.lexware.io/v1");
+ }
+
+ @Test
+ void buildWithoutHostThrowsException() {
+ RestUriBuilder builder = new RestUriBuilder()
+ .protocol("https")
+ .path("/contacts");
+
+ assertThatThrownBy(builder::build)
+ .isInstanceOf(NullPointerException.class);
+ }
+
+ @Test
+ void hostCannotBeNull() {
+ RestUriBuilder builder = new RestUriBuilder();
+
+ assertThatThrownBy(() -> builder.host(null))
+ .isInstanceOf(NullPointerException.class);
+ }
+
+ @Test
+ void protocolCannotBeNull() {
+ RestUriBuilder builder = new RestUriBuilder();
+
+ assertThatThrownBy(() -> builder.protocol(null))
+ .isInstanceOf(NullPointerException.class);
+ }
+
+ @Test
+ void pathCannotBeNull() {
+ RestUriBuilder builder = new RestUriBuilder();
+
+ assertThatThrownBy(() -> builder.path(null))
+ .isInstanceOf(NullPointerException.class);
+ }
+
+ @Test
+ void appendPathCannotBeNull() {
+ RestUriBuilder builder = new RestUriBuilder();
+
+ assertThatThrownBy(() -> builder.appendPath(null))
+ .isInstanceOf(NullPointerException.class);
+ }
+
+ @Test
+ void parameterKeyCannotBeNull() {
+ RestUriBuilder builder = new RestUriBuilder();
+
+ assertThatThrownBy(() -> builder.addParameter(null, "Berliner Kindl GmbH"))
+ .isInstanceOf(NullPointerException.class);
+ }
+
+ @Test
+ void parameterWithNullValueIncludesKeyOnly() {
+ // Spring's UriComponentsBuilder accepts null values and includes the key without a value
+ URI uri = new RestUriBuilder()
+ .protocol("https")
+ .host(LEXOFFICE_API_HOST)
+ .path("/contacts")
+ .addParameter("archived", null)
+ .addParameter("customer", "true")
+ .build();
+
+ assertThat(uri.toString()).isEqualTo("https://api.lexware.io/v1/contacts?archived&customer=true");
+ }
+
+ @Test
+ void parametersCollectionCannotBeNull() {
+ RestUriBuilder builder = new RestUriBuilder();
+
+ assertThatThrownBy(() -> builder.addParameters("voucherStatus", null))
+ .isInstanceOf(NullPointerException.class);
+ }
+
+ @Test
+ void builderIsFluent() {
+ RestUriBuilder builder = new RestUriBuilder();
+
+ RestUriBuilder result = builder
+ .protocol("https")
+ .host(LEXOFFICE_API_HOST)
+ .path("/contacts")
+ .appendPath("/be9475a4-ef80-442b-95eb-e56a7b2d5596")
+ .addParameter("page", 0);
+
+ assertThat(result).isSameAs(builder);
+ }
+}
diff --git a/src/test/java/de/focus_shift/lexoffice/java/sdk/SearchStringEncoderTest.java b/src/test/java/de/focus_shift/lexoffice/java/sdk/SearchStringEncoderTest.java
new file mode 100644
index 0000000..3ad90a3
--- /dev/null
+++ b/src/test/java/de/focus_shift/lexoffice/java/sdk/SearchStringEncoderTest.java
@@ -0,0 +1,98 @@
+package de.focus_shift.lexoffice.java.sdk;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.jupiter.api.Test;
+
+class SearchStringEncoderTest {
+
+ @Test
+ void htmlEncodeReturnsNullForNullInput() {
+ assertThat(SearchStringEncoder.htmlEncode(null)).isNull();
+ }
+
+ @Test
+ void htmlEncodeReturnsEmptyStringForEmptyInput() {
+ assertThat(SearchStringEncoder.htmlEncode("")).isEqualTo("");
+ }
+
+ @Test
+ void htmlEncodeReturnsUnchangedStringWithoutSpecialCharacters() {
+ assertThat(SearchStringEncoder.htmlEncode("Berliner Kindl GmbH"))
+ .isEqualTo("Berliner Kindl GmbH");
+ }
+
+ @Test
+ void htmlEncodeEncodesAmpersand() {
+ assertThat(SearchStringEncoder.htmlEncode("johnson & partner"))
+ .isEqualTo("johnson & partner");
+ }
+
+ @Test
+ void htmlEncodeEncodesLessThan() {
+ assertThat(SearchStringEncoder.htmlEncode("value < 100"))
+ .isEqualTo("value < 100");
+ }
+
+ @Test
+ void htmlEncodeEncodesGreaterThan() {
+ assertThat(SearchStringEncoder.htmlEncode("value > 50"))
+ .isEqualTo("value > 50");
+ }
+
+ @Test
+ void htmlEncodeEncodesMultipleSpecialCharacters() {
+ assertThat(SearchStringEncoder.htmlEncode("a < b & c > d"))
+ .isEqualTo("a < b & c > d");
+ }
+
+ @Test
+ void htmlEncodeEncodesMultipleAmpersands() {
+ assertThat(SearchStringEncoder.htmlEncode("Müller & Söhne & Partner GmbH"))
+ .isEqualTo("Müller & Söhne & Partner GmbH");
+ }
+
+ @Test
+ void htmlEncodePreservesUmlauts() {
+ assertThat(SearchStringEncoder.htmlEncode("Müller & Söhne"))
+ .isEqualTo("Müller & Söhne");
+ }
+
+ @Test
+ void htmlEncodeDoesNotDoubleEncodeAlreadyEncodedAmpersand() {
+ // If someone passes already encoded text, it will be encoded again
+ // This is expected behavior - the input should be raw text
+ assertThat(SearchStringEncoder.htmlEncode("&"))
+ .isEqualTo("&");
+ }
+
+ @Test
+ void htmlEncodeHandlesOnlyAmpersand() {
+ assertThat(SearchStringEncoder.htmlEncode("&"))
+ .isEqualTo("&");
+ }
+
+ @Test
+ void htmlEncodeHandlesOnlyLessThan() {
+ assertThat(SearchStringEncoder.htmlEncode("<"))
+ .isEqualTo("<");
+ }
+
+ @Test
+ void htmlEncodeHandlesOnlyGreaterThan() {
+ assertThat(SearchStringEncoder.htmlEncode(">"))
+ .isEqualTo(">");
+ }
+
+ @Test
+ void htmlEncodeHandlesConsecutiveSpecialCharacters() {
+ assertThat(SearchStringEncoder.htmlEncode("<<&&>>"))
+ .isEqualTo("<<&&>>");
+ }
+
+ @Test
+ void htmlEncodePreservesWhitespace() {
+ assertThat(SearchStringEncoder.htmlEncode(" Schmidt & Partner "))
+ .isEqualTo(" Schmidt & Partner ");
+ }
+}
diff --git a/src/test/java/de/focus_shift/lexoffice/java/sdk/chain/ContactChainIntegrationTest.java b/src/test/java/de/focus_shift/lexoffice/java/sdk/chain/ContactChainIntegrationTest.java
new file mode 100644
index 0000000..ba47eba
--- /dev/null
+++ b/src/test/java/de/focus_shift/lexoffice/java/sdk/chain/ContactChainIntegrationTest.java
@@ -0,0 +1,437 @@
+package de.focus_shift.lexoffice.java.sdk.chain;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.put;
+import static com.github.tomakehurst.wiremock.client.WireMock.putRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.verify;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
+import com.github.tomakehurst.wiremock.junit5.WireMockTest;
+import de.focus_shift.lexoffice.java.sdk.LexofficeApi;
+import de.focus_shift.lexoffice.java.sdk.LexofficeApiBuilder;
+import de.focus_shift.lexoffice.java.sdk.model.Company;
+import de.focus_shift.lexoffice.java.sdk.model.Contact;
+import de.focus_shift.lexoffice.java.sdk.model.ItemCreatedResult;
+import de.focus_shift.lexoffice.java.sdk.model.Page;
+import de.focus_shift.lexoffice.java.sdk.model.Roles;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+@WireMockTest
+class ContactChainIntegrationTest {
+
+ private LexofficeApi lexofficeApi;
+
+ @BeforeEach
+ void setUp(WireMockRuntimeInfo wmRuntimeInfo) {
+ String wiremockHost = wmRuntimeInfo.getHttpBaseUrl().replace("http://", "") + "/v1";
+
+ lexofficeApi =
+ new LexofficeApiBuilder().apiToken("test-api-key").protocol("http").host(wiremockHost).build();
+ }
+
+ @Test
+ void getContact() {
+ String contactId = "be9475a4-ef80-442b-95eb-e56a7b2d5596";
+
+ stubFor(
+ get(urlPathEqualTo("/v1/contacts/" + contactId))
+ .willReturn(
+ aResponse()
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ """
+ {
+ "id": "be9475a4-ef80-442b-95eb-e56a7b2d5596",
+ "organizationId": "aa93e8a8-2aa3-470b-b914-caad8a255dd8",
+ "version": 1,
+ "roles": {
+ "customer": {
+ "number": 10001
+ }
+ },
+ "company": {
+ "name": "Berliner Kindl GmbH",
+ "taxNumber": "12345/67890",
+ "vatRegistrationId": "DE123456789",
+ "allowTaxFreeInvoices": false,
+ "contactPersons": [
+ {
+ "salutation": "Herr",
+ "firstName": "Max",
+ "lastName": "Mustermann",
+ "primary": true,
+ "emailAddress": "max.mustermann@example.com",
+ "phoneNumber": "+49 30 123456"
+ }
+ ]
+ },
+ "addresses": {
+ "billing": [
+ {
+ "supplement": "Hinterhaus",
+ "street": "Juliusstraße 25",
+ "zip": "12051",
+ "city": "Berlin",
+ "countryCode": "DE"
+ }
+ ]
+ },
+ "emailAddresses": {
+ "business": ["info@example.com"],
+ "office": ["office@example.com"]
+ },
+ "phoneNumbers": {
+ "business": ["+49 30 123456"]
+ },
+ "archived": false
+ }
+ """)));
+
+ Contact contact = lexofficeApi.contact().get(contactId);
+
+ assertThat(contact).isNotNull();
+ assertThat(contact.getId()).isEqualTo(contactId);
+ assertThat(contact.getCompany().getName()).isEqualTo("Berliner Kindl GmbH");
+ assertThat(contact.getCompany().getVatRegistrationId()).isEqualTo("DE123456789");
+ assertThat(contact.getRoles().getCustomer()).isNotNull();
+ assertThat(contact.getRoles().getCustomer().getNumber()).isEqualTo(10001L);
+ }
+
+ @Test
+ void fetchContacts() {
+ stubFor(
+ get(urlPathEqualTo("/v1/contacts"))
+ .willReturn(
+ aResponse()
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ """
+ {
+ "content": [
+ {
+ "id": "be9475a4-ef80-442b-95eb-e56a7b2d5596",
+ "organizationId": "aa93e8a8-2aa3-470b-b914-caad8a255dd8",
+ "version": 1,
+ "roles": {
+ "customer": { "number": 10001 }
+ },
+ "company": {
+ "name": "Berliner Kindl GmbH"
+ },
+ "archived": false
+ },
+ {
+ "id": "c1234567-ef80-442b-95eb-e56a7b2d5596",
+ "organizationId": "aa93e8a8-2aa3-470b-b914-caad8a255dd8",
+ "version": 2,
+ "roles": {
+ "customer": { "number": 10002 }
+ },
+ "company": {
+ "name": "Test GmbH"
+ },
+ "archived": false
+ }
+ ],
+ "first": true,
+ "last": true,
+ "totalPages": 1,
+ "totalElements": 2,
+ "numberOfElements": 2,
+ "size": 25,
+ "number": 0
+ }
+ """)));
+
+ Page contactPage = lexofficeApi.contact().fetch().get();
+
+ assertThat(contactPage).isNotNull();
+ assertThat(contactPage.getContent()).hasSize(2);
+ assertThat(contactPage.getTotalElements()).isEqualTo(2);
+ assertThat(contactPage.getContent().get(0).getCompany().getName()).isEqualTo("Berliner Kindl GmbH");
+ assertThat(contactPage.getContent().get(1).getCompany().getName()).isEqualTo("Test GmbH");
+ }
+
+ @Test
+ void fetchContactsFilteredByCustomer() {
+ stubFor(
+ get(urlPathEqualTo("/v1/contacts"))
+ .withQueryParam("customer", equalTo("true"))
+ .willReturn(
+ aResponse()
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ """
+ {
+ "content": [
+ {
+ "id": "be9475a4-ef80-442b-95eb-e56a7b2d5596",
+ "version": 1,
+ "roles": {
+ "customer": { "number": 10001 }
+ },
+ "company": {
+ "name": "Berliner Kindl GmbH"
+ },
+ "archived": false
+ }
+ ],
+ "first": true,
+ "last": true,
+ "totalPages": 1,
+ "totalElements": 1,
+ "numberOfElements": 1,
+ "size": 25,
+ "number": 0
+ }
+ """)));
+
+ Page contactPage = lexofficeApi.contact().fetch().customer(true).get();
+
+ assertThat(contactPage).isNotNull();
+ assertThat(contactPage.getContent()).hasSize(1);
+ assertThat(contactPage.getContent().get(0).getRoles().getCustomer()).isNotNull();
+ }
+
+ @Test
+ void fetchContactsFilteredByName() {
+ stubFor(
+ get(urlPathEqualTo("/v1/contacts"))
+ .withQueryParam("name", equalTo("Berliner"))
+ .willReturn(
+ aResponse()
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ """
+ {
+ "content": [
+ {
+ "id": "be9475a4-ef80-442b-95eb-e56a7b2d5596",
+ "version": 1,
+ "roles": {
+ "customer": { "number": 10001 }
+ },
+ "company": {
+ "name": "Berliner Kindl GmbH"
+ },
+ "archived": false
+ }
+ ],
+ "first": true,
+ "last": true,
+ "totalPages": 1,
+ "totalElements": 1,
+ "numberOfElements": 1,
+ "size": 25,
+ "number": 0
+ }
+ """)));
+
+ Page contactPage = lexofficeApi.contact().fetch().name("Berliner").get();
+
+ assertThat(contactPage).isNotNull();
+ assertThat(contactPage.getContent()).hasSize(1);
+ assertThat(contactPage.getContent().get(0).getCompany().getName()).contains("Berliner");
+ }
+
+ @Test
+ void fetchContactsFilteredByNameWithSpecialCharacters() {
+ String companyName = "Müller & Söhne GmbH";
+ String htmlEncodedName = "Müller & Söhne GmbH";
+
+ stubFor(
+ get(urlPathEqualTo("/v1/contacts"))
+ .withQueryParam("name", equalTo(htmlEncodedName))
+ .willReturn(
+ aResponse()
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ """
+ {
+ "content": [
+ {
+ "id": "special-char-uuid",
+ "version": 1,
+ "roles": {
+ "customer": { "number": 10003 }
+ },
+ "company": {
+ "name": "Müller & Söhne GmbH"
+ },
+ "archived": false
+ }
+ ],
+ "first": true,
+ "last": true,
+ "totalPages": 1,
+ "totalElements": 1,
+ "numberOfElements": 1,
+ "size": 25,
+ "number": 0
+ }
+ """)));
+
+ Page contactPage = lexofficeApi.contact().fetch().name(companyName).get();
+
+ assertThat(contactPage).isNotNull();
+ assertThat(contactPage.getContent()).hasSize(1);
+ assertThat(contactPage.getContent().get(0).getCompany().getName()).isEqualTo(companyName);
+ }
+
+ @Test
+ void fetchContactsWithPagination() {
+ stubFor(
+ get(urlPathEqualTo("/v1/contacts"))
+ .withQueryParam("page", equalTo("0"))
+ .withQueryParam("size", equalTo("10"))
+ .willReturn(
+ aResponse()
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ """
+ {
+ "content": [
+ {
+ "id": "be9475a4-ef80-442b-95eb-e56a7b2d5596",
+ "version": 1,
+ "company": {
+ "name": "Berliner Kindl GmbH"
+ },
+ "archived": false
+ }
+ ],
+ "first": true,
+ "last": false,
+ "totalPages": 5,
+ "totalElements": 50,
+ "numberOfElements": 10,
+ "size": 10,
+ "number": 0
+ }
+ """)));
+
+ Page contactPage = lexofficeApi.contact().fetch().page(0).pageSize(10).get();
+
+ assertThat(contactPage).isNotNull();
+ assertThat(contactPage.getTotalPages()).isEqualTo(5);
+ assertThat(contactPage.getTotalElements()).isEqualTo(50);
+ assertThat(contactPage.getSize()).isEqualTo(10);
+ assertThat(contactPage.getFirst()).isTrue();
+ assertThat(contactPage.getLast()).isFalse();
+ }
+
+ @Test
+ void createContact() {
+ stubFor(
+ post(urlPathEqualTo("/v1/contacts"))
+ .willReturn(
+ aResponse()
+ .withStatus(201)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ """
+ {
+ "id": "e1234567-ef80-442b-95eb-e56a7b2d5596",
+ "resourceUri": "https://api.lexoffice.io/v1/contacts/e1234567-ef80-442b-95eb-e56a7b2d5596",
+ "createdDate": "2023-06-20T10:30:00.000+02:00",
+ "updatedDate": "2023-06-20T10:30:00.000+02:00",
+ "version": 0
+ }
+ """)));
+
+ Contact newContact =
+ Contact.builder()
+ .version(0L)
+ .company(Company.builder().name("Neue Firma GmbH").build())
+ .roles(Roles.builder().build())
+ .build();
+
+ ItemCreatedResult result = lexofficeApi.contact().create(newContact);
+
+ assertThat(result).isNotNull();
+ assertThat(result.getId()).isEqualTo("e1234567-ef80-442b-95eb-e56a7b2d5596");
+ assertThat(result.getResourceUri()).contains("/v1/contacts/");
+ assertThat(result.getVersion()).isEqualTo(0L);
+
+ verify(
+ postRequestedFor(urlPathEqualTo("/v1/contacts"))
+ .withRequestBody(
+ equalToJson(
+ """
+ {
+ "version": 0,
+ "company": {
+ "name": "Neue Firma GmbH"
+ },
+ "roles": {}
+ }
+ """,
+ true,
+ true)));
+ }
+
+ @Test
+ void updateContact() {
+ String contactId = "be9475a4-ef80-442b-95eb-e56a7b2d5596";
+
+ stubFor(
+ put(urlPathEqualTo("/v1/contacts/" + contactId))
+ .willReturn(
+ aResponse()
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ """
+ {
+ "id": "be9475a4-ef80-442b-95eb-e56a7b2d5596",
+ "organizationId": "aa93e8a8-2aa3-470b-b914-caad8a255dd8",
+ "version": 2,
+ "roles": {
+ "customer": { "number": 10001 }
+ },
+ "company": {
+ "name": "Berliner Kindl GmbH - Updated"
+ },
+ "archived": false
+ }
+ """)));
+
+ Contact contactToUpdate =
+ Contact.builder()
+ .id(contactId)
+ .version(1L)
+ .company(Company.builder().name("Berliner Kindl GmbH - Updated").build())
+ .build();
+
+ Contact updatedContact = lexofficeApi.contact().update(contactToUpdate);
+
+ assertThat(updatedContact).isNotNull();
+ assertThat(updatedContact.getId()).isEqualTo(contactId);
+ assertThat(updatedContact.getVersion()).isEqualTo(2L);
+ assertThat(updatedContact.getCompany().getName()).isEqualTo("Berliner Kindl GmbH - Updated");
+
+ verify(
+ putRequestedFor(urlPathEqualTo("/v1/contacts/" + contactId))
+ .withRequestBody(
+ equalToJson(
+ """
+ {
+ "id": "be9475a4-ef80-442b-95eb-e56a7b2d5596",
+ "version": 1,
+ "company": {
+ "name": "Berliner Kindl GmbH - Updated"
+ }
+ }
+ """,
+ true,
+ true)));
+ }
+}
diff --git a/src/test/java/de/focus_shift/lexoffice/java/sdk/chain/EventSubscriptionChainIntegrationTest.java b/src/test/java/de/focus_shift/lexoffice/java/sdk/chain/EventSubscriptionChainIntegrationTest.java
new file mode 100644
index 0000000..1b69677
--- /dev/null
+++ b/src/test/java/de/focus_shift/lexoffice/java/sdk/chain/EventSubscriptionChainIntegrationTest.java
@@ -0,0 +1,173 @@
+package de.focus_shift.lexoffice.java.sdk.chain;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.delete;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.verify;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
+import com.github.tomakehurst.wiremock.junit5.WireMockTest;
+import de.focus_shift.lexoffice.java.sdk.LexofficeApi;
+import de.focus_shift.lexoffice.java.sdk.LexofficeApiBuilder;
+import de.focus_shift.lexoffice.java.sdk.model.EventSubscription;
+import de.focus_shift.lexoffice.java.sdk.model.EventType;
+import de.focus_shift.lexoffice.java.sdk.model.ItemCreatedResult;
+import java.util.List;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+@WireMockTest
+class EventSubscriptionChainIntegrationTest {
+
+ private LexofficeApi lexofficeApi;
+
+ @BeforeEach
+ void setUp(WireMockRuntimeInfo wmRuntimeInfo) {
+ String wiremockHost = wmRuntimeInfo.getHttpBaseUrl().replace("http://", "") + "/v1";
+
+ lexofficeApi =
+ new LexofficeApiBuilder().apiToken("test-api-key").protocol("http").host(wiremockHost).build();
+ }
+
+ @Test
+ void getEventSubscription() {
+ String subscriptionId = "6a5c555e-060c-4fde-9ac3-4c7a396e5e1e";
+
+ stubFor(
+ get(urlPathEqualTo("/v1/event-subscriptions/" + subscriptionId))
+ .willReturn(
+ aResponse()
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ """
+ {
+ "subscriptionId": "6a5c555e-060c-4fde-9ac3-4c7a396e5e1e",
+ "organizationId": "aa93e8a8-2aa3-470b-b914-caad8a255dd8",
+ "createdDate": "2023-06-10T12:30:45.123+02:00",
+ "eventType": "invoice.created",
+ "callbackUrl": "https://example.com/webhook/invoice"
+ }
+ """)));
+
+ EventSubscription subscription = lexofficeApi.eventSubscriptions().get(subscriptionId);
+
+ assertThat(subscription).isNotNull();
+ assertThat(subscription.getSubscriptionId()).isEqualTo(subscriptionId);
+ assertThat(subscription.getEventType()).isEqualTo(EventType.INVOICE_CREATED);
+ assertThat(subscription.getCallbackUrl()).isEqualTo("https://example.com/webhook/invoice");
+ }
+
+ @Test
+ void getAllEventSubscriptions() {
+ stubFor(
+ get(urlPathEqualTo("/v1/event-subscriptions"))
+ .willReturn(
+ aResponse()
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ """
+ {
+ "content": [
+ {
+ "subscriptionId": "6a5c555e-060c-4fde-9ac3-4c7a396e5e1e",
+ "organizationId": "aa93e8a8-2aa3-470b-b914-caad8a255dd8",
+ "createdDate": "2023-06-10T12:30:45.123+02:00",
+ "eventType": "invoice.created",
+ "callbackUrl": "https://example.com/webhook/invoice"
+ },
+ {
+ "subscriptionId": "7b6d666f-171d-5gef-0bd4-5d8b407f6f2f",
+ "organizationId": "aa93e8a8-2aa3-470b-b914-caad8a255dd8",
+ "createdDate": "2023-06-11T09:15:30.456+02:00",
+ "eventType": "contact.created",
+ "callbackUrl": "https://example.com/webhook/contact"
+ },
+ {
+ "subscriptionId": "8c7e777g-282e-6hfg-1ce5-6e9c518g7g3g",
+ "organizationId": "aa93e8a8-2aa3-470b-b914-caad8a255dd8",
+ "createdDate": "2023-06-12T14:45:00.789+02:00",
+ "eventType": "quotation.status.changed",
+ "callbackUrl": "https://example.com/webhook/quotation"
+ }
+ ],
+ "first": true,
+ "last": true,
+ "totalPages": 1,
+ "totalElements": 3,
+ "numberOfElements": 3,
+ "size": 25,
+ "number": 0
+ }
+ """)));
+
+ List subscriptions = lexofficeApi.eventSubscriptions().getAll();
+
+ assertThat(subscriptions).hasSize(3);
+ assertThat(subscriptions.get(0).getEventType()).isEqualTo(EventType.INVOICE_CREATED);
+ assertThat(subscriptions.get(1).getEventType()).isEqualTo(EventType.CONTACT_CREATED);
+ assertThat(subscriptions.get(2).getEventType()).isEqualTo(EventType.QUOTATION_STATUS_CHANGED);
+ }
+
+ @Test
+ void createEventSubscription() {
+ stubFor(
+ post(urlPathEqualTo("/v1/event-subscriptions"))
+ .willReturn(
+ aResponse()
+ .withStatus(201)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ """
+ {
+ "id": "6a5c555e-060c-4fde-9ac3-4c7a396e5e1e",
+ "resourceUri": "https://api.lexoffice.io/v1/event-subscriptions/6a5c555e-060c-4fde-9ac3-4c7a396e5e1e",
+ "createdDate": "2023-06-10T12:30:45.123+02:00",
+ "updatedDate": "2023-06-10T12:30:45.123+02:00",
+ "version": 0
+ }
+ """)));
+
+ EventSubscription newSubscription =
+ EventSubscription.builder()
+ .eventType(EventType.INVOICE_CREATED)
+ .callbackUrl("https://example.com/webhook/invoice")
+ .build();
+
+ ItemCreatedResult result = lexofficeApi.eventSubscriptions().create(newSubscription);
+
+ assertThat(result).isNotNull();
+ assertThat(result.getId()).isEqualTo("6a5c555e-060c-4fde-9ac3-4c7a396e5e1e");
+ assertThat(result.getResourceUri()).contains("/v1/event-subscriptions/");
+ assertThat(result.getVersion()).isEqualTo(0L);
+
+ verify(
+ postRequestedFor(urlPathEqualTo("/v1/event-subscriptions"))
+ .withRequestBody(
+ equalToJson(
+ """
+ {
+ "eventType": "invoice.created",
+ "callbackUrl": "https://example.com/webhook/invoice"
+ }
+ """,
+ true,
+ true)));
+ }
+
+ @Test
+ void deleteEventSubscription() {
+ String subscriptionId = "6a5c555e-060c-4fde-9ac3-4c7a396e5e1e";
+
+ stubFor(
+ delete(urlPathEqualTo("/v1/event-subscriptions/" + subscriptionId))
+ .willReturn(aResponse().withStatus(204)));
+
+ lexofficeApi.eventSubscriptions().delete(subscriptionId);
+ }
+}
diff --git a/src/test/java/de/focus_shift/lexoffice/java/sdk/chain/InvoiceChainIntegrationTest.java b/src/test/java/de/focus_shift/lexoffice/java/sdk/chain/InvoiceChainIntegrationTest.java
new file mode 100644
index 0000000..ebc1846
--- /dev/null
+++ b/src/test/java/de/focus_shift/lexoffice/java/sdk/chain/InvoiceChainIntegrationTest.java
@@ -0,0 +1,212 @@
+package de.focus_shift.lexoffice.java.sdk.chain;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.verify;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
+import com.github.tomakehurst.wiremock.junit5.WireMockTest;
+import de.focus_shift.lexoffice.java.sdk.LexofficeApi;
+import de.focus_shift.lexoffice.java.sdk.LexofficeApiBuilder;
+import de.focus_shift.lexoffice.java.sdk.model.Invoice;
+import de.focus_shift.lexoffice.java.sdk.model.VoucherStatus;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+@WireMockTest
+class InvoiceChainIntegrationTest {
+
+ private LexofficeApi lexofficeApi;
+
+ @BeforeEach
+ void setUp(WireMockRuntimeInfo wmRuntimeInfo) {
+ String wiremockHost = wmRuntimeInfo.getHttpBaseUrl().replace("http://", "") + "/v1";
+
+ lexofficeApi =
+ new LexofficeApiBuilder().apiToken("test-api-key").protocol("http").host(wiremockHost).build();
+ }
+
+ @Test
+ void getInvoice() {
+ String invoiceId = "e9066f04-8cc7-4616-93f8-ac9571ec5e11";
+
+ stubFor(
+ get(urlPathEqualTo("/v1/invoices/" + invoiceId))
+ .willReturn(
+ aResponse()
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ """
+ {
+ "id": "e9066f04-8cc7-4616-93f8-ac9571ec5e11",
+ "organizationId": "aa93e8a8-2aa3-470b-b914-caad8a255dd8",
+ "createdDate": "2023-06-17T18:32:07.480+02:00",
+ "updatedDate": "2023-06-17T18:32:07.551+02:00",
+ "version": 1,
+ "language": "de",
+ "archived": false,
+ "voucherStatus": "open",
+ "voucherNumber": "RE1019",
+ "voucherDate": "2023-02-22T00:00:00.000+01:00",
+ "dueDate": "2023-03-08T00:00:00.000+01:00",
+ "address": {
+ "contactId": "97c5794f-8ab2-43ad-b459-c5980b055e4d",
+ "name": "Berliner Kindl GmbH",
+ "street": "Juliusstraße 25",
+ "zip": "12051",
+ "city": "Berlin",
+ "countryCode": "DE"
+ },
+ "lineItems": [
+ {
+ "id": "97b98491-e953-4dc9-97a9-ae437a8052b4",
+ "type": "custom",
+ "name": "Axa Rahmenschloss Defender RL",
+ "description": "Vollständig symmetrisches Design in metallicfarbener Ausführung. Der ergonomische Bedienkopf mit praktischem Zahlenfeld.",
+ "quantity": 1.0000,
+ "unitName": "Stück",
+ "unitPrice": {
+ "currency": "EUR",
+ "netAmount": 20.08,
+ "grossAmount": 23.90,
+ "taxRatePercentage": 19
+ },
+ "lineItemAmount": 23.90
+ }
+ ],
+ "totalPrice": {
+ "currency": "EUR",
+ "totalNetAmount": 20.08,
+ "totalGrossAmount": 23.90,
+ "totalTaxAmount": 3.82
+ },
+ "taxAmounts": [
+ {
+ "taxRatePercentage": 19,
+ "taxAmount": 3.82,
+ "netAmount": 20.08
+ }
+ ],
+ "taxConditions": {
+ "taxType": "gross"
+ },
+ "paymentConditions": {
+ "paymentTermLabel": "10 Tage - 3 %, 30 Tage netto",
+ "paymentTermDuration": 30,
+ "paymentDiscountConditions": {
+ "discountPercentage": 3,
+ "discountRange": 10
+ }
+ },
+ "title": "Rechnung",
+ "introduction": "Ihre bestellten Positionen stellen wir Ihnen hiermit in Rechnung",
+ "remark": "Vielen Dank für Ihren Einkauf"
+ }
+ """)));
+
+ Invoice invoice = lexofficeApi.invoice().get(invoiceId);
+
+ assertThat(invoice).isNotNull();
+ assertThat(invoice.getId()).isEqualTo(invoiceId);
+ assertThat(invoice.getVoucherNumber()).isEqualTo("RE1019");
+ assertThat(invoice.getVoucherStatus()).isEqualTo(VoucherStatus.OPEN);
+ assertThat(invoice.getAddress().getName()).isEqualTo("Berliner Kindl GmbH");
+ assertThat(invoice.getLineItems()).hasSize(1);
+ assertThat(invoice.getLineItems().get(0).getName()).isEqualTo("Axa Rahmenschloss Defender RL");
+ }
+
+ @Test
+ void createInvoice() {
+ stubFor(
+ post(urlPathEqualTo("/v1/invoices"))
+ .willReturn(
+ aResponse()
+ .withStatus(201)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ """
+ {
+ "id": "e9066f04-8cc7-4616-93f8-ac9571ec5e11",
+ "organizationId": "aa93e8a8-2aa3-470b-b914-caad8a255dd8",
+ "createdDate": "2023-06-17T18:32:07.480+02:00",
+ "updatedDate": "2023-06-17T18:32:07.551+02:00",
+ "version": 0,
+ "voucherStatus": "draft"
+ }
+ """)));
+
+ Invoice newInvoice = Invoice.builder().title("Rechnung").language("de").build();
+
+ Invoice createdInvoice = lexofficeApi.invoice().create().submit(newInvoice);
+
+ assertThat(createdInvoice).isNotNull();
+ assertThat(createdInvoice.getId()).isEqualTo("e9066f04-8cc7-4616-93f8-ac9571ec5e11");
+ assertThat(createdInvoice.getVoucherStatus()).isEqualTo(VoucherStatus.DRAFT);
+
+ verify(
+ postRequestedFor(urlPathEqualTo("/v1/invoices"))
+ .withRequestBody(
+ equalToJson(
+ """
+ {
+ "title": "Rechnung",
+ "language": "de"
+ }
+ """,
+ true,
+ true)));
+ }
+
+ @Test
+ void createInvoiceFinalized() {
+ stubFor(
+ post(urlPathEqualTo("/v1/invoices"))
+ .withQueryParam("finalize", equalTo("true"))
+ .willReturn(
+ aResponse()
+ .withStatus(201)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ """
+ {
+ "id": "e9066f04-8cc7-4616-93f8-ac9571ec5e11",
+ "organizationId": "aa93e8a8-2aa3-470b-b914-caad8a255dd8",
+ "createdDate": "2023-06-17T18:32:07.480+02:00",
+ "updatedDate": "2023-06-17T18:32:07.551+02:00",
+ "version": 0,
+ "voucherStatus": "open",
+ "voucherNumber": "RE1020"
+ }
+ """)));
+
+ Invoice newInvoice = Invoice.builder().title("Rechnung").language("de").build();
+
+ Invoice createdInvoice = lexofficeApi.invoice().create().finalize(true).submit(newInvoice);
+
+ assertThat(createdInvoice).isNotNull();
+ assertThat(createdInvoice.getId()).isEqualTo("e9066f04-8cc7-4616-93f8-ac9571ec5e11");
+ assertThat(createdInvoice.getVoucherStatus()).isEqualTo(VoucherStatus.OPEN);
+ assertThat(createdInvoice.getVoucherNumber()).isEqualTo("RE1020");
+
+ verify(
+ postRequestedFor(urlPathEqualTo("/v1/invoices"))
+ .withQueryParam("finalize", equalTo("true"))
+ .withRequestBody(
+ equalToJson(
+ """
+ {
+ "title": "Rechnung",
+ "language": "de"
+ }
+ """,
+ true,
+ true)));
+ }
+}
diff --git a/src/test/java/de/focus_shift/lexoffice/java/sdk/chain/QuotationChainIntegrationTest.java b/src/test/java/de/focus_shift/lexoffice/java/sdk/chain/QuotationChainIntegrationTest.java
new file mode 100644
index 0000000..7706f01
--- /dev/null
+++ b/src/test/java/de/focus_shift/lexoffice/java/sdk/chain/QuotationChainIntegrationTest.java
@@ -0,0 +1,202 @@
+package de.focus_shift.lexoffice.java.sdk.chain;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.verify;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
+import com.github.tomakehurst.wiremock.junit5.WireMockTest;
+import de.focus_shift.lexoffice.java.sdk.LexofficeApi;
+import de.focus_shift.lexoffice.java.sdk.LexofficeApiBuilder;
+import de.focus_shift.lexoffice.java.sdk.model.ItemCreatedResult;
+import de.focus_shift.lexoffice.java.sdk.model.Quotation;
+import de.focus_shift.lexoffice.java.sdk.model.VoucherStatus;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+@WireMockTest
+class QuotationChainIntegrationTest {
+
+ private LexofficeApi lexofficeApi;
+
+ @BeforeEach
+ void setUp(WireMockRuntimeInfo wmRuntimeInfo) {
+ String wiremockHost = wmRuntimeInfo.getHttpBaseUrl().replace("http://", "") + "/v1";
+
+ lexofficeApi =
+ new LexofficeApiBuilder().apiToken("test-api-key").protocol("http").host(wiremockHost).build();
+ }
+
+ @Test
+ void getQuotation() {
+ String quotationId = "424f784e-1f4e-439e-8f71-19673e6a8b9d";
+
+ stubFor(
+ get(urlPathEqualTo("/v1/quotations/" + quotationId))
+ .willReturn(
+ aResponse()
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ """
+ {
+ "id": "424f784e-1f4e-439e-8f71-19673e6a8b9d",
+ "organizationId": "aa93e8a8-2aa3-470b-b914-caad8a255dd8",
+ "createdDate": "2023-04-11T14:15:22.123+02:00",
+ "updatedDate": "2023-04-11T14:17:45.456+02:00",
+ "version": 2,
+ "language": "de",
+ "archived": false,
+ "voucherStatus": "open",
+ "voucherNumber": "AG0021",
+ "voucherDate": "2023-04-11T00:00:00.000+02:00",
+ "expirationDate": "2023-05-11T00:00:00.000+02:00",
+ "address": {
+ "contactId": "97c5794f-8ab2-43ad-b459-c5980b055e4d",
+ "name": "Muster GmbH",
+ "street": "Musterstraße 42",
+ "zip": "12345",
+ "city": "Musterstadt",
+ "countryCode": "DE"
+ },
+ "lineItems": [
+ {
+ "id": "68569bfc-e5ae-472d-bbdf-6d51a82b1d2f",
+ "type": "custom",
+ "name": "Beratungsleistung",
+ "description": "Projektberatung und Konzepterstellung",
+ "quantity": 8.0000,
+ "unitName": "Stunden",
+ "unitPrice": {
+ "currency": "EUR",
+ "netAmount": 150.00,
+ "grossAmount": 178.50,
+ "taxRatePercentage": 19
+ },
+ "lineItemAmount": 1428.00
+ }
+ ],
+ "totalPrice": {
+ "currency": "EUR",
+ "totalNetAmount": 1200.00,
+ "totalGrossAmount": 1428.00,
+ "totalTaxAmount": 228.00
+ },
+ "taxAmounts": [
+ {
+ "taxRatePercentage": 19,
+ "taxAmount": 228.00,
+ "netAmount": 1200.00
+ }
+ ],
+ "taxConditions": {
+ "taxType": "gross"
+ },
+ "title": "Angebot",
+ "introduction": "Gerne unterbreiten wir Ihnen folgendes Angebot",
+ "remark": "Dieses Angebot ist 30 Tage gültig"
+ }
+ """)));
+
+ Quotation quotation = lexofficeApi.quotation().get(quotationId);
+
+ assertThat(quotation).isNotNull();
+ assertThat(quotation.getId()).isEqualTo(quotationId);
+ assertThat(quotation.getVoucherNumber()).isEqualTo("AG0021");
+ assertThat(quotation.getVoucherStatus()).isEqualTo(VoucherStatus.OPEN);
+ assertThat(quotation.getAddress().getName()).isEqualTo("Muster GmbH");
+ assertThat(quotation.getLineItems()).hasSize(1);
+ assertThat(quotation.getLineItems().get(0).getName()).isEqualTo("Beratungsleistung");
+ }
+
+ @Test
+ void createQuotation() {
+ stubFor(
+ post(urlPathEqualTo("/v1/quotations"))
+ .willReturn(
+ aResponse()
+ .withStatus(201)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ """
+ {
+ "id": "424f784e-1f4e-439e-8f71-19673e6a8b9d",
+ "resourceUri": "https://api.lexoffice.io/v1/quotations/424f784e-1f4e-439e-8f71-19673e6a8b9d",
+ "createdDate": "2023-04-11T14:15:22.123+02:00",
+ "updatedDate": "2023-04-11T14:15:22.123+02:00",
+ "version": 0
+ }
+ """)));
+
+ Quotation newQuotation = Quotation.builder().title("Angebot").language("de").build();
+
+ ItemCreatedResult result = lexofficeApi.quotation().create().submit(newQuotation);
+
+ assertThat(result).isNotNull();
+ assertThat(result.getId()).isEqualTo("424f784e-1f4e-439e-8f71-19673e6a8b9d");
+ assertThat(result.getResourceUri()).contains("/v1/quotations/424f784e-1f4e-439e-8f71-19673e6a8b9d");
+ assertThat(result.getVersion()).isEqualTo(0L);
+
+ verify(
+ postRequestedFor(urlPathEqualTo("/v1/quotations"))
+ .withRequestBody(
+ equalToJson(
+ """
+ {
+ "title": "Angebot",
+ "language": "de"
+ }
+ """,
+ true,
+ true)));
+ }
+
+ @Test
+ void createQuotationFinalized() {
+ stubFor(
+ post(urlPathEqualTo("/v1/quotations"))
+ .withQueryParam("finalize", equalTo("true"))
+ .willReturn(
+ aResponse()
+ .withStatus(201)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ """
+ {
+ "id": "424f784e-1f4e-439e-8f71-19673e6a8b9d",
+ "resourceUri": "https://api.lexoffice.io/v1/quotations/424f784e-1f4e-439e-8f71-19673e6a8b9d",
+ "createdDate": "2023-04-11T14:15:22.123+02:00",
+ "updatedDate": "2023-04-11T14:15:22.123+02:00",
+ "version": 1
+ }
+ """)));
+
+ Quotation newQuotation = Quotation.builder().title("Angebot").language("de").build();
+
+ ItemCreatedResult result = lexofficeApi.quotation().create().finalize(true).submit(newQuotation);
+
+ assertThat(result).isNotNull();
+ assertThat(result.getId()).isEqualTo("424f784e-1f4e-439e-8f71-19673e6a8b9d");
+ assertThat(result.getVersion()).isEqualTo(1L);
+
+ verify(
+ postRequestedFor(urlPathEqualTo("/v1/quotations"))
+ .withQueryParam("finalize", equalTo("true"))
+ .withRequestBody(
+ equalToJson(
+ """
+ {
+ "title": "Angebot",
+ "language": "de"
+ }
+ """,
+ true,
+ true)));
+ }
+}
diff --git a/src/test/java/de/focus_shift/lexoffice/java/sdk/chain/RecurringTemplateChainIntegrationTest.java b/src/test/java/de/focus_shift/lexoffice/java/sdk/chain/RecurringTemplateChainIntegrationTest.java
new file mode 100644
index 0000000..7e194ff
--- /dev/null
+++ b/src/test/java/de/focus_shift/lexoffice/java/sdk/chain/RecurringTemplateChainIntegrationTest.java
@@ -0,0 +1,282 @@
+package de.focus_shift.lexoffice.java.sdk.chain;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
+import com.github.tomakehurst.wiremock.junit5.WireMockTest;
+import de.focus_shift.lexoffice.java.sdk.LexofficeApi;
+import de.focus_shift.lexoffice.java.sdk.LexofficeApiBuilder;
+import de.focus_shift.lexoffice.java.sdk.model.Page;
+import de.focus_shift.lexoffice.java.sdk.model.RecurringTemplate;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+@WireMockTest
+class RecurringTemplateChainIntegrationTest {
+
+ private LexofficeApi lexofficeApi;
+
+ @BeforeEach
+ void setUp(WireMockRuntimeInfo wmRuntimeInfo) {
+ String wiremockHost = wmRuntimeInfo.getHttpBaseUrl().replace("http://", "") + "/v1";
+
+ lexofficeApi =
+ new LexofficeApiBuilder().apiToken("test-api-key").protocol("http").host(wiremockHost).build();
+ }
+
+ @Test
+ void getRecurringTemplate() {
+ String templateId = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
+
+ stubFor(
+ get(urlPathEqualTo("/v1/recurring-templates/" + templateId))
+ .willReturn(
+ aResponse()
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ """
+ {
+ "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "organizationId": "aa93e8a8-2aa3-470b-b914-caad8a255dd8",
+ "createdDate": "2023-01-15T10:30:00.000+01:00",
+ "updatedDate": "2023-06-01T14:45:30.123+02:00",
+ "version": 5,
+ "language": "de",
+ "archived": false,
+ "address": {
+ "contactId": "97c5794f-8ab2-43ad-b459-c5980b055e4d",
+ "name": "Stammkunde GmbH",
+ "street": "Hauptstraße 1",
+ "zip": "10115",
+ "city": "Berlin",
+ "countryCode": "DE"
+ },
+ "lineItems": [
+ {
+ "id": "item-uuid-1234",
+ "type": "custom",
+ "name": "Monatliche Wartung",
+ "description": "Regelmäßige Systemwartung und Updates",
+ "quantity": 1.0000,
+ "unitName": "Pauschale",
+ "unitPrice": {
+ "currency": "EUR",
+ "netAmount": 500.00,
+ "grossAmount": 595.00,
+ "taxRatePercentage": 19
+ },
+ "lineItemAmount": 595.00
+ }
+ ],
+ "totalPrice": {
+ "currency": "EUR",
+ "totalNetAmount": 500.00,
+ "totalGrossAmount": 595.00,
+ "totalTaxAmount": 95.00
+ },
+ "taxAmounts": [
+ {
+ "taxRatePercentage": 19,
+ "taxAmount": 95.00,
+ "netAmount": 500.00
+ }
+ ],
+ "taxConditions": {
+ "taxType": "gross"
+ },
+ "paymentConditions": {
+ "paymentTermLabel": "Zahlbar innerhalb von 14 Tagen",
+ "paymentTermDuration": 14
+ },
+ "title": "Wartungsvertrag",
+ "introduction": "Hiermit stellen wir Ihnen die vereinbarte Wartungspauschale in Rechnung",
+ "remark": "Vielen Dank für Ihr Vertrauen",
+ "recurringTemplateSettings": {
+ "id": "settings-uuid-5678",
+ "startDate": "2023-01-01T00:00:00.000+01:00",
+ "endDate": "2024-12-31T00:00:00.000+01:00",
+ "finalize": true,
+ "shippingType": "service",
+ "executionInterval": "MONTHLY",
+ "executionStatus": "ACTIVE",
+ "lastExecutionFailed": false,
+ "lastExecutionErrorMessage": null,
+ "nextExecutionDate": "2023-07-01T00:00:00.000+02:00"
+ }
+ }
+ """)));
+
+ RecurringTemplate template = lexofficeApi.recurringTemplates().get(templateId);
+
+ assertThat(template).isNotNull();
+ assertThat(template.getId()).isEqualTo(templateId);
+ assertThat(template.getTitle()).isEqualTo("Wartungsvertrag");
+ assertThat(template.getAddress().getName()).isEqualTo("Stammkunde GmbH");
+ assertThat(template.getLineItems()).hasSize(1);
+ assertThat(template.getLineItems().get(0).getName()).isEqualTo("Monatliche Wartung");
+ assertThat(template.getRecurringTemplateSettings()).isNotNull();
+ }
+
+ @Test
+ void fetchRecurringTemplates() {
+ stubFor(
+ get(urlPathEqualTo("/v1/recurring-templates"))
+ .willReturn(
+ aResponse()
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ """
+ {
+ "content": [
+ {
+ "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "organizationId": "aa93e8a8-2aa3-470b-b914-caad8a255dd8",
+ "createdDate": "2023-01-15T10:30:00.000+01:00",
+ "updatedDate": "2023-06-01T14:45:30.123+02:00",
+ "version": 5,
+ "language": "de",
+ "archived": false,
+ "title": "Wartungsvertrag",
+ "address": {
+ "name": "Stammkunde GmbH"
+ }
+ },
+ {
+ "id": "b2c3d4e5-f6g7-8901-bcde-f23456789012",
+ "organizationId": "aa93e8a8-2aa3-470b-b914-caad8a255dd8",
+ "createdDate": "2023-02-20T09:00:00.000+01:00",
+ "updatedDate": "2023-05-15T16:30:00.456+02:00",
+ "version": 3,
+ "language": "de",
+ "archived": false,
+ "title": "Hosting-Abonnement",
+ "address": {
+ "name": "Web Services AG"
+ }
+ }
+ ],
+ "first": true,
+ "last": true,
+ "totalPages": 1,
+ "totalElements": 2,
+ "numberOfElements": 2,
+ "size": 25,
+ "number": 0
+ }
+ """)));
+
+ Page templatePage = lexofficeApi.recurringTemplates().fetch().get();
+
+ assertThat(templatePage).isNotNull();
+ assertThat(templatePage.getContent()).hasSize(2);
+ assertThat(templatePage.getTotalElements()).isEqualTo(2);
+ assertThat(templatePage.getFirst()).isTrue();
+ assertThat(templatePage.getLast()).isTrue();
+
+ assertThat(templatePage.getContent().get(0).getTitle()).isEqualTo("Wartungsvertrag");
+ assertThat(templatePage.getContent().get(1).getTitle()).isEqualTo("Hosting-Abonnement");
+ }
+
+ @Test
+ void fetchRecurringTemplatesWithPagination() {
+ stubFor(
+ get(urlPathEqualTo("/v1/recurring-templates"))
+ .withQueryParam("page", equalTo("0"))
+ .withQueryParam("size", equalTo("10"))
+ .willReturn(
+ aResponse()
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ """
+ {
+ "content": [
+ {
+ "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "organizationId": "aa93e8a8-2aa3-470b-b914-caad8a255dd8",
+ "createdDate": "2023-01-15T10:30:00.000+01:00",
+ "updatedDate": "2023-06-01T14:45:30.123+02:00",
+ "version": 5,
+ "language": "de",
+ "archived": false,
+ "title": "Wartungsvertrag"
+ }
+ ],
+ "first": true,
+ "last": false,
+ "totalPages": 3,
+ "totalElements": 25,
+ "numberOfElements": 10,
+ "size": 10,
+ "number": 0
+ }
+ """)));
+
+ Page templatePage =
+ lexofficeApi.recurringTemplates().fetch().page(0).pageSize(10).get();
+
+ assertThat(templatePage).isNotNull();
+ assertThat(templatePage.getTotalPages()).isEqualTo(3);
+ assertThat(templatePage.getTotalElements()).isEqualTo(25);
+ assertThat(templatePage.getSize()).isEqualTo(10);
+ assertThat(templatePage.getFirst()).isTrue();
+ assertThat(templatePage.getLast()).isFalse();
+ }
+
+ @Test
+ void fetchRecurringTemplatesWithSorting() {
+ stubFor(
+ get(urlPathEqualTo("/v1/recurring-templates"))
+ .withQueryParam("sort", equalTo("createdDate,DESC"))
+ .willReturn(
+ aResponse()
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ """
+ {
+ "content": [
+ {
+ "id": "b2c3d4e5-f6g7-8901-bcde-f23456789012",
+ "organizationId": "aa93e8a8-2aa3-470b-b914-caad8a255dd8",
+ "createdDate": "2023-02-20T09:00:00.000+01:00",
+ "updatedDate": "2023-05-15T16:30:00.456+02:00",
+ "version": 3,
+ "language": "de",
+ "archived": false,
+ "title": "Hosting-Abonnement"
+ },
+ {
+ "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "organizationId": "aa93e8a8-2aa3-470b-b914-caad8a255dd8",
+ "createdDate": "2023-01-15T10:30:00.000+01:00",
+ "updatedDate": "2023-06-01T14:45:30.123+02:00",
+ "version": 5,
+ "language": "de",
+ "archived": false,
+ "title": "Wartungsvertrag"
+ }
+ ],
+ "first": true,
+ "last": true,
+ "totalPages": 1,
+ "totalElements": 2,
+ "numberOfElements": 2,
+ "size": 25,
+ "number": 0
+ }
+ """)));
+
+ Page templatePage =
+ lexofficeApi.recurringTemplates().fetch().sortByCreatedDate(false).get();
+
+ assertThat(templatePage).isNotNull();
+ assertThat(templatePage.getContent()).hasSize(2);
+ // First item should be the more recently created one (descending order)
+ assertThat(templatePage.getContent().get(0).getTitle()).isEqualTo("Hosting-Abonnement");
+ assertThat(templatePage.getContent().get(1).getTitle()).isEqualTo("Wartungsvertrag");
+ }
+}
diff --git a/src/test/java/de/focus_shift/lexoffice/java/sdk/chain/VoucherListChainIntegrationTest.java b/src/test/java/de/focus_shift/lexoffice/java/sdk/chain/VoucherListChainIntegrationTest.java
new file mode 100644
index 0000000..b5fa9ab
--- /dev/null
+++ b/src/test/java/de/focus_shift/lexoffice/java/sdk/chain/VoucherListChainIntegrationTest.java
@@ -0,0 +1,263 @@
+package de.focus_shift.lexoffice.java.sdk.chain;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
+import com.github.tomakehurst.wiremock.junit5.WireMockTest;
+import de.focus_shift.lexoffice.java.sdk.LexofficeApi;
+import de.focus_shift.lexoffice.java.sdk.LexofficeApiBuilder;
+import de.focus_shift.lexoffice.java.sdk.model.Page;
+import de.focus_shift.lexoffice.java.sdk.model.Voucher;
+import de.focus_shift.lexoffice.java.sdk.model.VoucherStatus;
+import de.focus_shift.lexoffice.java.sdk.model.VoucherType;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+@WireMockTest
+class VoucherListChainIntegrationTest {
+
+ private LexofficeApi lexofficeApi;
+
+ @BeforeEach
+ void setUp(WireMockRuntimeInfo wmRuntimeInfo) {
+ String wiremockHost = wmRuntimeInfo.getHttpBaseUrl().replace("http://", "") + "/v1";
+
+ lexofficeApi =
+ new LexofficeApiBuilder().apiToken("test-api-key").protocol("http").host(wiremockHost).build();
+ }
+
+ @Test
+ void getVoucherList() {
+ stubFor(
+ get(urlPathEqualTo("/v1/voucherlist"))
+ .withQueryParam("voucherType", equalTo("invoice"))
+ .withQueryParam("voucherStatus", equalTo("open"))
+ .willReturn(
+ aResponse()
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ """
+ {
+ "content": [
+ {
+ "id": "e9066f04-8cc7-4616-93f8-ac9571ec5e11",
+ "voucherType": "invoice",
+ "voucherStatus": "open",
+ "voucherNumber": "RE1019",
+ "voucherDate": "2023-02-22T00:00:00.000+01:00",
+ "createdDate": "2023-06-17T18:32:07.480+02:00",
+ "updatedDate": "2023-06-17T18:32:07.551+02:00",
+ "dueDate": "2023-03-08T00:00:00.000+01:00",
+ "contactId": "97c5794f-8ab2-43ad-b459-c5980b055e4d",
+ "contactName": "Berliner Kindl GmbH",
+ "totalAmount": 23.90,
+ "openAmount": 23.90,
+ "currency": "EUR",
+ "archived": false
+ },
+ {
+ "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "voucherType": "invoice",
+ "voucherStatus": "open",
+ "voucherNumber": "RE1020",
+ "voucherDate": "2023-02-23T00:00:00.000+01:00",
+ "createdDate": "2023-06-18T10:15:30.123+02:00",
+ "updatedDate": "2023-06-18T10:15:30.456+02:00",
+ "dueDate": "2023-03-09T00:00:00.000+01:00",
+ "contactId": "12345678-abcd-ef12-3456-7890abcdef12",
+ "contactName": "Test Kunde AG",
+ "totalAmount": 1190.00,
+ "openAmount": 500.00,
+ "currency": "EUR",
+ "archived": false
+ }
+ ],
+ "first": true,
+ "last": true,
+ "totalPages": 1,
+ "totalElements": 2,
+ "numberOfElements": 2,
+ "size": 25,
+ "number": 0,
+ "sort": [
+ {
+ "property": "voucherDate",
+ "direction": "DESC"
+ }
+ ]
+ }
+ """)));
+
+ Page voucherPage =
+ lexofficeApi.voucherList().voucherType(VoucherType.INVOICE).voucherStatus(VoucherStatus.OPEN).get();
+
+ assertThat(voucherPage).isNotNull();
+ assertThat(voucherPage.getContent()).hasSize(2);
+ assertThat(voucherPage.getTotalElements()).isEqualTo(2);
+ assertThat(voucherPage.getFirst()).isTrue();
+ assertThat(voucherPage.getLast()).isTrue();
+
+ Voucher firstVoucher = voucherPage.getContent().get(0);
+ assertThat(firstVoucher.getVoucherNumber()).isEqualTo("RE1019");
+ assertThat(firstVoucher.getVoucherType()).isEqualTo(VoucherType.INVOICE);
+ assertThat(firstVoucher.getVoucherStatus()).isEqualTo(VoucherStatus.OPEN);
+ assertThat(firstVoucher.getContactName()).isEqualTo("Berliner Kindl GmbH");
+ }
+
+ @Test
+ void getVoucherListWithPagination() {
+ stubFor(
+ get(urlPathEqualTo("/v1/voucherlist"))
+ .withQueryParam("voucherType", equalTo("invoice"))
+ .withQueryParam("voucherStatus", equalTo("open"))
+ .withQueryParam("page", equalTo("0"))
+ .withQueryParam("size", equalTo("10"))
+ .willReturn(
+ aResponse()
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ """
+ {
+ "content": [
+ {
+ "id": "e9066f04-8cc7-4616-93f8-ac9571ec5e11",
+ "voucherType": "invoice",
+ "voucherStatus": "open",
+ "voucherNumber": "RE1019",
+ "voucherDate": "2023-02-22T00:00:00.000+01:00",
+ "contactName": "Berliner Kindl GmbH",
+ "totalAmount": 23.90,
+ "currency": "EUR",
+ "archived": false
+ }
+ ],
+ "first": true,
+ "last": false,
+ "totalPages": 5,
+ "totalElements": 50,
+ "numberOfElements": 10,
+ "size": 10,
+ "number": 0
+ }
+ """)));
+
+ Page voucherPage =
+ lexofficeApi
+ .voucherList()
+ .voucherType(VoucherType.INVOICE)
+ .voucherStatus(VoucherStatus.OPEN)
+ .page(0)
+ .pageSize(10)
+ .get();
+
+ assertThat(voucherPage).isNotNull();
+ assertThat(voucherPage.getTotalPages()).isEqualTo(5);
+ assertThat(voucherPage.getTotalElements()).isEqualTo(50);
+ assertThat(voucherPage.getSize()).isEqualTo(10);
+ assertThat(voucherPage.getNumber()).isEqualTo(0);
+ assertThat(voucherPage.getFirst()).isTrue();
+ assertThat(voucherPage.getLast()).isFalse();
+ }
+
+ @Test
+ void getVoucherListWithSorting() {
+ stubFor(
+ get(urlPathEqualTo("/v1/voucherlist"))
+ .withQueryParam("voucherType", equalTo("invoice"))
+ .withQueryParam("voucherStatus", equalTo("open"))
+ .withQueryParam("sort", equalTo("voucherDate,DESC"))
+ .willReturn(
+ aResponse()
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ """
+ {
+ "content": [
+ {
+ "id": "e9066f04-8cc7-4616-93f8-ac9571ec5e11",
+ "voucherType": "invoice",
+ "voucherStatus": "open",
+ "voucherNumber": "RE1019",
+ "voucherDate": "2023-02-22T00:00:00.000+01:00",
+ "contactName": "Berliner Kindl GmbH",
+ "totalAmount": 23.90,
+ "currency": "EUR",
+ "archived": false
+ }
+ ],
+ "first": true,
+ "last": true,
+ "totalPages": 1,
+ "totalElements": 1,
+ "numberOfElements": 1,
+ "size": 25,
+ "number": 0
+ }
+ """)));
+
+ Page voucherPage =
+ lexofficeApi
+ .voucherList()
+ .voucherType(VoucherType.INVOICE)
+ .voucherStatus(VoucherStatus.OPEN)
+ .sortByVoucherDate(false)
+ .get();
+
+ assertThat(voucherPage).isNotNull();
+ assertThat(voucherPage.getContent()).hasSize(1);
+ }
+
+ @Test
+ void getVoucherListFilteredByContactName() {
+ stubFor(
+ get(urlPathEqualTo("/v1/voucherlist"))
+ .withQueryParam("voucherType", equalTo("invoice"))
+ .withQueryParam("voucherStatus", equalTo("open"))
+ .withQueryParam("contactName", equalTo("Berliner Kindl"))
+ .willReturn(
+ aResponse()
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ """
+ {
+ "content": [
+ {
+ "id": "e9066f04-8cc7-4616-93f8-ac9571ec5e11",
+ "voucherType": "invoice",
+ "voucherStatus": "open",
+ "voucherNumber": "RE1019",
+ "voucherDate": "2023-02-22T00:00:00.000+01:00",
+ "contactName": "Berliner Kindl GmbH",
+ "totalAmount": 23.90,
+ "currency": "EUR",
+ "archived": false
+ }
+ ],
+ "first": true,
+ "last": true,
+ "totalPages": 1,
+ "totalElements": 1,
+ "numberOfElements": 1,
+ "size": 25,
+ "number": 0
+ }
+ """)));
+
+ Page voucherPage =
+ lexofficeApi
+ .voucherList()
+ .voucherType(VoucherType.INVOICE)
+ .voucherStatus(VoucherStatus.OPEN)
+ .contactName("Berliner Kindl")
+ .get();
+
+ assertThat(voucherPage).isNotNull();
+ assertThat(voucherPage.getContent()).hasSize(1);
+ assertThat(voucherPage.getContent().get(0).getContactName()).isEqualTo("Berliner Kindl GmbH");
+ }
+}