From 4197b90de8f468dc7fc1a7739553fe88276156b0 Mon Sep 17 00:00:00 2001 From: Johannes Graf Date: Tue, 20 Jan 2026 23:24:08 +0100 Subject: [PATCH] add support for Recurring Templates --- README.md | 2 +- .../lexoffice/java/sdk/LexofficeApi.java | 5 + .../sdk/chain/RecurringTemplateChain.java | 127 ++++++++++ .../java/sdk/model/ExecutionInterval.java | 20 ++ .../java/sdk/model/ExecutionStatus.java | 19 ++ .../java/sdk/model/PaymentConditions.java | 3 + .../java/sdk/model/RecurringTemplate.java | 74 ++++++ .../sdk/model/RecurringTemplateSettings.java | 57 +++++ .../java/sdk/model/TaxConditions.java | 3 + .../lexoffice/java/sdk/model/TaxType.java | 6 +- .../java/sdk/chain/InvoiceChainTest.java | 2 +- .../sdk/chain/RecurringTemplateChainTest.java | 54 +++++ .../java/sdk/model/RecurringTemplateTest.java | 224 ++++++++++++++++++ 13 files changed, 593 insertions(+), 3 deletions(-) create mode 100644 src/main/java/de/focus_shift/lexoffice/java/sdk/chain/RecurringTemplateChain.java create mode 100644 src/main/java/de/focus_shift/lexoffice/java/sdk/model/ExecutionInterval.java create mode 100644 src/main/java/de/focus_shift/lexoffice/java/sdk/model/ExecutionStatus.java create mode 100644 src/main/java/de/focus_shift/lexoffice/java/sdk/model/RecurringTemplate.java create mode 100644 src/main/java/de/focus_shift/lexoffice/java/sdk/model/RecurringTemplateSettings.java create mode 100644 src/test/java/de/focus_shift/lexoffice/java/sdk/chain/RecurringTemplateChainTest.java create mode 100644 src/test/java/de/focus_shift/lexoffice/java/sdk/model/RecurringTemplateTest.java diff --git a/README.md b/README.md index d6ce6fc..61467b2 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ The following Endpoints are implemented based on lexoffice developer documentati * [ ] Posting Categories Endpoint * [ ] Profile Endpoint * [x] Quotations -* [ ] Recurring Templates Endpoint +* [x] Recurring Templates Endpoint * [x] Voucherlist Endpoint * [ ] Vouchers Endpoint diff --git a/src/main/java/de/focus_shift/lexoffice/java/sdk/LexofficeApi.java b/src/main/java/de/focus_shift/lexoffice/java/sdk/LexofficeApi.java index d8bf8a1..6ab4da9 100644 --- a/src/main/java/de/focus_shift/lexoffice/java/sdk/LexofficeApi.java +++ b/src/main/java/de/focus_shift/lexoffice/java/sdk/LexofficeApi.java @@ -4,6 +4,7 @@ import de.focus_shift.lexoffice.java.sdk.chain.EventSubscriptionChain; import de.focus_shift.lexoffice.java.sdk.chain.InvoiceChain; import de.focus_shift.lexoffice.java.sdk.chain.QuotationChain; +import de.focus_shift.lexoffice.java.sdk.chain.RecurringTemplateChain; import de.focus_shift.lexoffice.java.sdk.chain.VoucherListChain; import java.text.DateFormat; @@ -40,4 +41,8 @@ public EventSubscriptionChain eventSubscriptions() { return new EventSubscriptionChain(context); } + public RecurringTemplateChain recurringTemplates() { + return new RecurringTemplateChain(context); + } + } diff --git a/src/main/java/de/focus_shift/lexoffice/java/sdk/chain/RecurringTemplateChain.java b/src/main/java/de/focus_shift/lexoffice/java/sdk/chain/RecurringTemplateChain.java new file mode 100644 index 0000000..9909277 --- /dev/null +++ b/src/main/java/de/focus_shift/lexoffice/java/sdk/chain/RecurringTemplateChain.java @@ -0,0 +1,127 @@ +package de.focus_shift.lexoffice.java.sdk.chain; + +import de.focus_shift.lexoffice.java.sdk.RequestContext; +import de.focus_shift.lexoffice.java.sdk.model.Page; +import de.focus_shift.lexoffice.java.sdk.model.RecurringTemplate; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpMethod; + +@RequiredArgsConstructor +public class RecurringTemplateChain { + + private final RequestContext context; + + /** + * Retrieve a single recurring template by its ID. + * + * @param id the UUID of the recurring template + * @return the RecurringTemplate + */ + public RecurringTemplate get(String id) { + return new Get(context).get(id); + } + + /** + * Returns a Fetch chain for retrieving a paginated list of recurring templates. + * + * @return Fetch chain for method chaining + */ + public Fetch fetch() { + return new Fetch(context); + } + + protected static class Get extends ExecutableRequestChain { + private static final ParameterizedTypeReference TYPE_REFERENCE = + new ParameterizedTypeReference<>() {}; + + public Get(RequestContext context) { + super(context, "/recurring-templates"); + } + + @SneakyThrows + public RecurringTemplate get(String id) { + getUriBuilder().appendPath("/" + id); + return getContext().execute(getUriBuilder(), HttpMethod.GET, TYPE_REFERENCE); + } + } + + public static class Fetch extends ExecutableRequestChain { + private static final ParameterizedTypeReference> TYPE_REFERENCE = + new ParameterizedTypeReference<>() {}; + + public Fetch(RequestContext context) { + super(context, "/recurring-templates"); + } + + /** + * Pages are zero indexed, thus providing 0 for page will return the first page. + */ + public Fetch page(int page) { + super.getUriBuilder() + .addParameter("page", String.valueOf(page)); + return this; + } + + /** + * Default page size is set to 25 but can be increased up to 250. + */ + public Fetch pageSize(int pageSize) { + super.getUriBuilder() + .addParameter("size", String.valueOf(pageSize)); + return this; + } + + /** + * Sort by createdDate in ascending or descending order. + */ + public Fetch sortByCreatedDate(boolean asc) { + super.getUriBuilder() + .addParameter("sort", String.format("createdDate,%s", asc ? "ASC" : "DESC")); + return this; + } + + /** + * Sort by updatedDate in ascending or descending order. + */ + public Fetch sortByUpdatedDate(boolean asc) { + super.getUriBuilder() + .addParameter("sort", String.format("updatedDate,%s", asc ? "ASC" : "DESC")); + return this; + } + + /** + * Sort by lastExecutionDate in ascending or descending order. + */ + public Fetch sortByLastExecutionDate(boolean asc) { + super.getUriBuilder() + .addParameter("sort", String.format("lastExecutionDate,%s", asc ? "ASC" : "DESC")); + return this; + } + + /** + * Sort by nextExecutionDate in ascending or descending order. + */ + public Fetch sortByNextExecutionDate(boolean asc) { + super.getUriBuilder() + .addParameter("sort", String.format("nextExecutionDate,%s", asc ? "ASC" : "DESC")); + return this; + } + + /** + * Generic sort method for custom sort parameters. + * Format: "property,direction" e.g., "createdDate,ASC" + */ + public Fetch sort(String sort) { + super.getUriBuilder() + .addParameter("sort", sort); + return this; + } + + @SneakyThrows + public Page get() { + return getContext().execute(getUriBuilder(), HttpMethod.GET, TYPE_REFERENCE); + } + } +} 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 new file mode 100644 index 0000000..a406c01 --- /dev/null +++ b/src/main/java/de/focus_shift/lexoffice/java/sdk/model/ExecutionInterval.java @@ -0,0 +1,20 @@ +package de.focus_shift.lexoffice.java.sdk.model; + +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; + +public enum ExecutionInterval { + + WEEKLY("WEEKLY"), + MONTHLY("MONTHLY"), + QUARTERLY("QUARTERLY"), + ANNUALLY("ANNUALLY"); + + @Getter + @JsonValue + private String value; + + ExecutionInterval(String value) { + this.value = value; + } +} diff --git a/src/main/java/de/focus_shift/lexoffice/java/sdk/model/ExecutionStatus.java b/src/main/java/de/focus_shift/lexoffice/java/sdk/model/ExecutionStatus.java new file mode 100644 index 0000000..1c7694e --- /dev/null +++ b/src/main/java/de/focus_shift/lexoffice/java/sdk/model/ExecutionStatus.java @@ -0,0 +1,19 @@ +package de.focus_shift.lexoffice.java.sdk.model; + +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; + +public enum ExecutionStatus { + + ACTIVE("ACTIVE"), + PAUSED("PAUSED"), + ENDED("ENDED"); + + @Getter + @JsonValue + private String value; + + ExecutionStatus(String value) { + this.value = value; + } +} diff --git a/src/main/java/de/focus_shift/lexoffice/java/sdk/model/PaymentConditions.java b/src/main/java/de/focus_shift/lexoffice/java/sdk/model/PaymentConditions.java index b19c415..a354f40 100644 --- a/src/main/java/de/focus_shift/lexoffice/java/sdk/model/PaymentConditions.java +++ b/src/main/java/de/focus_shift/lexoffice/java/sdk/model/PaymentConditions.java @@ -15,6 +15,9 @@ public class PaymentConditions { @JsonProperty("paymentTermLabel") private String paymentTermLabel; + @JsonProperty("paymentTermLabelTemplate") + private String paymentTermLabelTemplate; + @JsonProperty("paymentTermDuration") private Integer paymentTermDuration; diff --git a/src/main/java/de/focus_shift/lexoffice/java/sdk/model/RecurringTemplate.java b/src/main/java/de/focus_shift/lexoffice/java/sdk/model/RecurringTemplate.java new file mode 100644 index 0000000..34ba91f --- /dev/null +++ b/src/main/java/de/focus_shift/lexoffice/java/sdk/model/RecurringTemplate.java @@ -0,0 +1,74 @@ +package de.focus_shift.lexoffice.java.sdk.model; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.Singular; + +import java.util.Date; +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class RecurringTemplate { + + @JsonProperty("id") + private String id; + + @JsonProperty("organizationId") + private String organizationId; + + @JsonProperty("createdDate") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") + private Date createdDate; + + @JsonProperty("updatedDate") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") + private Date updatedDate; + + @JsonProperty("version") + private Integer version; + + @JsonProperty("language") + private String language; + + @JsonProperty("archived") + private boolean archived; + + @JsonProperty("address") + private Address address; + + @Singular + @JsonProperty("lineItems") + private List lineItems; + + @JsonProperty("totalPrice") + private TotalPrice totalPrice; + + @Singular + @JsonProperty("taxAmounts") + private List taxAmounts; + + @JsonProperty("taxConditions") + private TaxConditions taxConditions; + + @JsonProperty("paymentConditions") + private PaymentConditions paymentConditions; + + @JsonProperty("title") + private String title; + + @JsonProperty("introduction") + private String introduction; + + @JsonProperty("remark") + private String remark; + + @JsonProperty("recurringTemplateSettings") + private RecurringTemplateSettings recurringTemplateSettings; +} diff --git a/src/main/java/de/focus_shift/lexoffice/java/sdk/model/RecurringTemplateSettings.java b/src/main/java/de/focus_shift/lexoffice/java/sdk/model/RecurringTemplateSettings.java new file mode 100644 index 0000000..c955c6f --- /dev/null +++ b/src/main/java/de/focus_shift/lexoffice/java/sdk/model/RecurringTemplateSettings.java @@ -0,0 +1,57 @@ +package de.focus_shift.lexoffice.java.sdk.model; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class RecurringTemplateSettings { + + @JsonProperty("id") + private String id; + + @JsonProperty("startDate") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + private Date startDate; + + @JsonProperty("endDate") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + private Date endDate; + + @JsonProperty("finalize") + private Boolean finalize; + + @JsonProperty("shippingType") + private ShippingType shippingType; + + @JsonProperty("retroactiveInvoice") + private Boolean retroactiveInvoice; + + @JsonProperty("executionInterval") + private ExecutionInterval executionInterval; + + @JsonProperty("lastExecutionFailed") + private Boolean lastExecutionFailed; + + @JsonProperty("lastExecutionErrorMessage") + private String lastExecutionErrorMessage; + + @JsonProperty("executionStatus") + private ExecutionStatus executionStatus; + + @JsonProperty("lastExecutionDate") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + private Date lastExecutionDate; + + @JsonProperty("nextExecutionDate") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + private Date nextExecutionDate; +} diff --git a/src/main/java/de/focus_shift/lexoffice/java/sdk/model/TaxConditions.java b/src/main/java/de/focus_shift/lexoffice/java/sdk/model/TaxConditions.java index a657122..c4f5ec8 100644 --- a/src/main/java/de/focus_shift/lexoffice/java/sdk/model/TaxConditions.java +++ b/src/main/java/de/focus_shift/lexoffice/java/sdk/model/TaxConditions.java @@ -18,4 +18,7 @@ public class TaxConditions { @JsonProperty("taxTypeNote") private String taxTypeNote; + @JsonProperty("taxSubType") + private String taxSubType; + } diff --git a/src/main/java/de/focus_shift/lexoffice/java/sdk/model/TaxType.java b/src/main/java/de/focus_shift/lexoffice/java/sdk/model/TaxType.java index 0a8ef12..1876d28 100644 --- a/src/main/java/de/focus_shift/lexoffice/java/sdk/model/TaxType.java +++ b/src/main/java/de/focus_shift/lexoffice/java/sdk/model/TaxType.java @@ -27,7 +27,11 @@ public enum TaxType { /** * Ausfuhrlieferungen an Drittländer */ - THIRD_PARTY_COUNTRY_DELIVERY("thirdPartyCountryDelivery"); + THIRD_PARTY_COUNTRY_DELIVERY("thirdPartyCountryDelivery"), + /** + * Photovoltaikanlagen + */ + PHOTOVOLTAIC_EQUIPMENT("photovoltaicEquipment"); @Getter @JsonValue diff --git a/src/test/java/de/focus_shift/lexoffice/java/sdk/chain/InvoiceChainTest.java b/src/test/java/de/focus_shift/lexoffice/java/sdk/chain/InvoiceChainTest.java index 4f1a2cb..febc967 100644 --- a/src/test/java/de/focus_shift/lexoffice/java/sdk/chain/InvoiceChainTest.java +++ b/src/test/java/de/focus_shift/lexoffice/java/sdk/chain/InvoiceChainTest.java @@ -52,7 +52,7 @@ void exampleUsage() { .shippingDate(new Date()) .shippingType(ShippingType.DELIVERY) .build()) - .taxConditions(new TaxConditions(TaxType.NET, "")) + .taxConditions(TaxConditions.builder().taxType(TaxType.NET).build()) .lineItems(Arrays.asList(LineItem.builder() .type(LineItemType.CUSTOM) .name("Name") diff --git a/src/test/java/de/focus_shift/lexoffice/java/sdk/chain/RecurringTemplateChainTest.java b/src/test/java/de/focus_shift/lexoffice/java/sdk/chain/RecurringTemplateChainTest.java new file mode 100644 index 0000000..ea2e7b4 --- /dev/null +++ b/src/test/java/de/focus_shift/lexoffice/java/sdk/chain/RecurringTemplateChainTest.java @@ -0,0 +1,54 @@ +package de.focus_shift.lexoffice.java.sdk.chain; + +import de.focus_shift.lexoffice.java.sdk.LexofficeApi; +import de.focus_shift.lexoffice.java.sdk.model.Address; +import de.focus_shift.lexoffice.java.sdk.model.ExecutionInterval; +import de.focus_shift.lexoffice.java.sdk.model.ExecutionStatus; +import de.focus_shift.lexoffice.java.sdk.model.RecurringTemplate; +import de.focus_shift.lexoffice.java.sdk.model.RecurringTemplateSettings; +import de.focus_shift.lexoffice.java.sdk.model.ShippingType; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.Date; + +class RecurringTemplateChainTest { + + @Test + @Disabled + void exampleUsage() { + LexofficeApi lexofficeApi = Mockito.mock(LexofficeApi.class); + + // Example of how the RecurringTemplate model looks + RecurringTemplate template = RecurringTemplate.builder() + .id("template-uuid") + .language("de") + .title("Monthly Invoice") + .introduction("Monthly recurring invoice") + .remark("Thank you for your business") + .address(Address.builder() + .contactId("contact-uuid") + .build()) + .recurringTemplateSettings(RecurringTemplateSettings.builder() + .startDate(new Date()) + .endDate(new Date()) + .finalize(true) + .shippingType(ShippingType.SERVICE) + .executionInterval(ExecutionInterval.MONTHLY) + .executionStatus(ExecutionStatus.ACTIVE) + .build()) + .build(); + + // GET single template + lexofficeApi.recurringTemplates().get("template-uuid"); + + // GET paginated list with sorting + lexofficeApi.recurringTemplates() + .fetch() + .page(0) + .pageSize(50) + .sortByNextExecutionDate(true) + .get(); + } +} diff --git a/src/test/java/de/focus_shift/lexoffice/java/sdk/model/RecurringTemplateTest.java b/src/test/java/de/focus_shift/lexoffice/java/sdk/model/RecurringTemplateTest.java new file mode 100644 index 0000000..2e13f2c --- /dev/null +++ b/src/test/java/de/focus_shift/lexoffice/java/sdk/model/RecurringTemplateTest.java @@ -0,0 +1,224 @@ +package de.focus_shift.lexoffice.java.sdk.model; + +import org.junit.jupiter.api.Test; +import tools.jackson.databind.json.JsonMapper; + +import java.math.BigDecimal; + +import static org.assertj.core.api.Assertions.assertThat; + +class RecurringTemplateTest { + + private final String json = """ + { + "id": "ac1d66a8-6d59-408b-9413-d56b1db7946f", + "organizationId": "aa93e8a8-2aa3-470b-b914-caad8a255dd8", + "createdDate": "2023-02-10T09:00:00.000+01:00", + "updatedDate": "2023-02-10T09:00:00.000+01:00", + "version": 0, + "language": "de", + "archived": false, + "address": { + "contactId": "464f4881-7a8c-4dc4-87de-7c6fd9a506b8", + "name": "Bike & Ride GmbH & Co. KG", + "supplement": "Gebäude 10", + "street": "Musterstraße 42", + "city": "Freiburg", + "zip": "79112", + "countryCode": "DE" + }, + "lineItems": [ + { + "id": "97b98491-e953-4dc9-97a9-ae437a8052b4", + "type": "material", + "name": "Abus Kabelschloss Primo 590 ", + "description": "· 9,5 mm starkes, smoke-mattes Spiralkabel mit integrierter Halterlösung zur Befestigung am Sattelklemmbolzen · bewährter Qualitäts-Schließzylinder mit praktischem Wendeschlüssel · KabelØ: 9,5 mm, Länge: 150 cm", + "quantity": 2, + "unitName": "Stück", + "unitPrice": { + "currency": "EUR", + "netAmount": 13.4, + "grossAmount": 15.95, + "taxRatePercentage": 19 + }, + "discountPercentage": 50, + "lineItemAmount": 13.4 + }, + { + "id": "dc4c805b-7df1-4310-a548-22be4499eb04", + "type": "service", + "name": "Aufwändige Montage", + "description": "Aufwand für arbeitsintensive Montagetätigkeit", + "quantity": 1, + "unitName": "Stunde", + "unitPrice": { + "currency": "EUR", + "netAmount": 8.32, + "grossAmount": 8.9, + "taxRatePercentage": 7 + }, + "discountPercentage": 0, + "lineItemAmount": 8.32 + }, + { + "id": null, + "type": "custom", + "name": "Energieriegel Testpaket", + "description": null, + "quantity": 1, + "unitName": "Stück", + "unitPrice": { + "currency": "EUR", + "netAmount": 5, + "grossAmount": 5, + "taxRatePercentage": 0 + }, + "discountPercentage": 0, + "lineItemAmount": 5 + }, + { + "type": "text", + "name": "Freitextposition", + "description": "This item type can contain either a name or a description or both." + } + ], + "totalPrice": { + "currency": "EUR", + "totalNetAmount": 26.72, + "totalGrossAmount": 29.85, + "totalTaxAmount": 3.13, + "totalDiscountAbsolute": null, + "totalDiscountPercentage": null + }, + "taxAmounts": [ + { + "taxRatePercentage": 0, + "taxAmount": 0, + "netAmount": 5 + }, + { + "taxRatePercentage": 7, + "taxAmount": 0.58, + "netAmount": 8.32 + }, + { + "taxRatePercentage": 19, + "taxAmount": 2.55, + "netAmount": 13.4 + } + ], + "taxConditions": { + "taxType": "net", + "taxTypeNote": null + }, + "paymentConditions": { + "paymentTermLabel": "10 Tage - 3 %, 30 Tage netto", + "paymentTermLabelTemplate": "{discountRange} Tage -{discount}, {paymentRange} 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", + "recurringTemplateSettings": { + "id": "9c5b8bde-7d36-49e8-af5c-4fbe7dc9fa01", + "startDate": "2023-03-01", + "endDate": "2023-06-30", + "finalize": true, + "shippingType": "service", + "retroactiveInvoice": false, + "executionInterval": "MONTHLY", + "nextExecutionDate": "2023-03-01", + "lastExecutionFailed": false, + "lastExecutionErrorMessage": null, + "executionStatus": "ACTIVE" + } + } + """; + + @Test + void deserializeRecurringTemplate() { + JsonMapper jsonMapper = JsonMapper.builder().build(); + + RecurringTemplate template = jsonMapper.readValue(json, RecurringTemplate.class); + + // Basic fields + assertThat(template.getId()).isEqualTo("ac1d66a8-6d59-408b-9413-d56b1db7946f"); + assertThat(template.getOrganizationId()).isEqualTo("aa93e8a8-2aa3-470b-b914-caad8a255dd8"); + assertThat(template.getVersion()).isEqualTo(0); + assertThat(template.getLanguage()).isEqualTo("de"); + assertThat(template.isArchived()).isFalse(); + assertThat(template.getCreatedDate()).isNotNull(); + assertThat(template.getUpdatedDate()).isNotNull(); + + // Address + assertThat(template.getAddress()).isNotNull(); + assertThat(template.getAddress().getContactId()).isEqualTo("464f4881-7a8c-4dc4-87de-7c6fd9a506b8"); + assertThat(template.getAddress().getName()).isEqualTo("Bike & Ride GmbH & Co. KG"); + assertThat(template.getAddress().getSupplement()).isEqualTo("Gebäude 10"); + assertThat(template.getAddress().getStreet()).isEqualTo("Musterstraße 42"); + assertThat(template.getAddress().getCity()).isEqualTo("Freiburg"); + assertThat(template.getAddress().getZip()).isEqualTo("79112"); + assertThat(template.getAddress().getCountryCode()).isEqualTo("DE"); + + // Line items + assertThat(template.getLineItems()).hasSize(4); + assertThat(template.getLineItems().get(0).getType()).isEqualTo(LineItemType.MATERIAL); + assertThat(template.getLineItems().get(0).getName()).isEqualTo("Abus Kabelschloss Primo 590 "); + assertThat(template.getLineItems().get(0).getQuantity()).isEqualByComparingTo(BigDecimal.valueOf(2)); + assertThat(template.getLineItems().get(0).getUnitPrice().getCurrency()).isEqualTo(Currency.EUR); + assertThat(template.getLineItems().get(0).getDiscountPercentage()).isEqualTo(50L); + + assertThat(template.getLineItems().get(1).getType()).isEqualTo(LineItemType.SERVICE); + assertThat(template.getLineItems().get(2).getType()).isEqualTo(LineItemType.CUSTOM); + assertThat(template.getLineItems().get(3).getType()).isEqualTo(LineItemType.TEXT); + + // Total price + assertThat(template.getTotalPrice()).isNotNull(); + assertThat(template.getTotalPrice().getCurrency()).isEqualTo(Currency.EUR); + assertThat(template.getTotalPrice().getTotalNetAmount()).isEqualByComparingTo(BigDecimal.valueOf(26.72)); + assertThat(template.getTotalPrice().getTotalGrossAmount()).isEqualByComparingTo(BigDecimal.valueOf(29.85)); + assertThat(template.getTotalPrice().getTotalTaxAmount()).isEqualByComparingTo(BigDecimal.valueOf(3.13)); + + // Tax amounts + assertThat(template.getTaxAmounts()).hasSize(3); + assertThat(template.getTaxAmounts().get(0).getTaxRatePercentage()).isEqualTo(0L); + assertThat(template.getTaxAmounts().get(1).getTaxRatePercentage()).isEqualTo(7L); + assertThat(template.getTaxAmounts().get(2).getTaxRatePercentage()).isEqualTo(19L); + + // Tax conditions + assertThat(template.getTaxConditions()).isNotNull(); + assertThat(template.getTaxConditions().getTaxType()).isEqualTo(TaxType.NET); + + // Payment conditions + assertThat(template.getPaymentConditions()).isNotNull(); + assertThat(template.getPaymentConditions().getPaymentTermLabel()).isEqualTo("10 Tage - 3 %, 30 Tage netto"); + assertThat(template.getPaymentConditions().getPaymentTermLabelTemplate()).isEqualTo("{discountRange} Tage -{discount}, {paymentRange} Tage netto"); + assertThat(template.getPaymentConditions().getPaymentTermDuration()).isEqualTo(30); + assertThat(template.getPaymentConditions().getPaymentDiscountConditions()).isNotNull(); + assertThat(template.getPaymentConditions().getPaymentDiscountConditions().getDiscountPercentage()).isEqualByComparingTo(BigDecimal.valueOf(3)); + assertThat(template.getPaymentConditions().getPaymentDiscountConditions().getDiscountRange()).isEqualTo(10); + + // Title, introduction, remark + assertThat(template.getTitle()).isEqualTo("Rechnung"); + assertThat(template.getIntroduction()).isEqualTo("Ihre bestellten Positionen stellen wir Ihnen hiermit in Rechnung"); + assertThat(template.getRemark()).isEqualTo("Vielen Dank für Ihren Einkauf"); + + // Recurring template settings + assertThat(template.getRecurringTemplateSettings()).isNotNull(); + assertThat(template.getRecurringTemplateSettings().getId()).isEqualTo("9c5b8bde-7d36-49e8-af5c-4fbe7dc9fa01"); + assertThat(template.getRecurringTemplateSettings().getStartDate()).isNotNull(); + assertThat(template.getRecurringTemplateSettings().getEndDate()).isNotNull(); + assertThat(template.getRecurringTemplateSettings().getFinalize()).isTrue(); + assertThat(template.getRecurringTemplateSettings().getShippingType()).isEqualTo(ShippingType.SERVICE); + assertThat(template.getRecurringTemplateSettings().getRetroactiveInvoice()).isFalse(); + assertThat(template.getRecurringTemplateSettings().getExecutionInterval()).isEqualTo(ExecutionInterval.MONTHLY); + assertThat(template.getRecurringTemplateSettings().getNextExecutionDate()).isNotNull(); + assertThat(template.getRecurringTemplateSettings().getLastExecutionFailed()).isFalse(); + assertThat(template.getRecurringTemplateSettings().getLastExecutionErrorMessage()).isNull(); + assertThat(template.getRecurringTemplateSettings().getExecutionStatus()).isEqualTo(ExecutionStatus.ACTIVE); + } +}