From ce79b20808edaa33b56b0a7b9fbc060b4f94e45b Mon Sep 17 00:00:00 2001 From: Jan Date: Sun, 2 Nov 2025 21:42:30 +0100 Subject: [PATCH] Update Java version to 23 and enhance tariff services with improved repository integration, optional return types, and unique constraints in the schema. --- pom.xml | 2 +- .../taric/controller/TariffController.java | 7 +- .../taric/controller/TariffController2.java | 212 +++---- .../de/avatic/taric/model/AppliedMeasure.java | 85 +-- .../taric/model/AppliedMeasureCondition.java | 85 +-- .../java/de/avatic/taric/model/Import.java | 61 +- .../java/de/avatic/taric/model/Measure.java | 1 + .../avatic/taric/model/MeasureExclusion.java | 8 +- .../avatic/taric/model/MeasureFootnote.java | 11 +- .../de/avatic/taric/model/Nomenclature.java | 2 + .../taric/repository/ImportRepository.java | 8 +- .../taric/service/NomenclatureService.java | 23 +- .../avatic/taric/service/TariffService.java | 91 ++- .../avatic/taric/service/TariffService2.java | 526 +++++++++--------- src/main/resources/application.properties | 1 + .../db/migration/V1__Create_schema.sql | 15 +- 16 files changed, 525 insertions(+), 613 deletions(-) diff --git a/pom.xml b/pom.xml index a3b6013..f5d19dc 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ - 17 + 23 diff --git a/src/main/java/de/avatic/taric/controller/TariffController.java b/src/main/java/de/avatic/taric/controller/TariffController.java index d0cc329..3cd7cd0 100644 --- a/src/main/java/de/avatic/taric/controller/TariffController.java +++ b/src/main/java/de/avatic/taric/controller/TariffController.java @@ -1,6 +1,7 @@ package de.avatic.taric.controller; import de.avatic.taric.service.TariffService; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -18,10 +19,8 @@ public class TariffController { } @GetMapping("") - public void getTariffRate(@RequestParam String hsCode, @RequestParam String countryCode) { - - tariffService.importTariffs( hsCode, countryCode); - + public ResponseEntity getTariffRate(@RequestParam String hsCode, @RequestParam String countryCode) { + return tariffService.importTariffs( hsCode, countryCode).map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build()); } } diff --git a/src/main/java/de/avatic/taric/controller/TariffController2.java b/src/main/java/de/avatic/taric/controller/TariffController2.java index 4667f3e..753a35f 100644 --- a/src/main/java/de/avatic/taric/controller/TariffController2.java +++ b/src/main/java/de/avatic/taric/controller/TariffController2.java @@ -1,7 +1,7 @@ package de.avatic.taric.controller; import de.avatic.taric.service.TariffService2; -import de.avatic.taric.service.TariffService2.TariffResult; +//import de.avatic.taric.service.TariffService2.TariffResult; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -23,109 +23,109 @@ import java.util.Map; @Tag(name = "Tariff API", description = "API zur Abfrage von Zolltarifen") public class TariffController2 { - private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TariffController2.class); - - private final TariffService2 tariffService; - - public TariffController2(TariffService2 tariffService) { - this.tariffService = tariffService; - } - - @GetMapping("/rate") - @Operation(summary = "Zolltarif abfragen", - description = "Ermittelt den Zolltarif für einen HS-Code und ein Herkunftsland") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Tarif erfolgreich gefunden"), - @ApiResponse(responseCode = "404", description = "Kein Tarif gefunden"), - @ApiResponse(responseCode = "400", description = "Ungültige Eingabeparameter") - }) - public ResponseEntity getTariffRate( - @Parameter(description = "HS-Code (6, 8 oder 10 Stellen)", example = "850410") - @RequestParam - @NotBlank(message = "HS Code ist erforderlich") - @Pattern(regexp = "^[0-9]{4,10}$", message = "HS Code muss 4-10 Ziffern enthalten") - String hsCode, - - @Parameter(description = "ISO-2 Ländercode", example = "CN") - @RequestParam - @NotBlank(message = "Ländercode ist erforderlich") - @Pattern(regexp = "^[A-Z]{2}$", message = "Ländercode muss 2 Großbuchstaben sein") - String countryCode) { - - log.info("Tariff rate request - HS Code: {}, Country: {}", hsCode, countryCode); - - try { - TariffResult result = tariffService.getTariffRate(hsCode, countryCode); - - if (result.isFound()) { - TariffResponse response = TariffResponse.success( - result.getRate(), - result.getHsCode(), - result.getCountryCode() - ); - return ResponseEntity.ok(response); - } else { - TariffResponse response = TariffResponse.notFound(result.getMessage()); - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response); - } - - } catch (Exception e) { - log.error("Error getting tariff rate", e); - TariffResponse response = TariffResponse.error("Internal server error: " + e.getMessage()); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); - } - } - - @GetMapping("/health") - @Operation(summary = "Health Check", description = "Prüft ob der Service verfügbar ist") - public ResponseEntity> health() { - Map response = new HashMap<>(); - response.put("status", "UP"); - response.put("service", "TARIC Tariff Service"); - return ResponseEntity.ok(response); - } - - /** - * Response-Klasse für Tarif-Abfragen - */ - public static class TariffResponse { - private final boolean success; - private final BigDecimal tariffRate; - private final String hsCode; - private final String countryCode; - private final String message; - private final String formattedRate; - - private TariffResponse(boolean success, BigDecimal tariffRate, - String hsCode, String countryCode, String message) { - this.success = success; - this.tariffRate = tariffRate; - this.hsCode = hsCode; - this.countryCode = countryCode; - this.message = message; - this.formattedRate = tariffRate != null ? - tariffRate.toString() + " %" : null; - } - - public static TariffResponse success(BigDecimal rate, String hsCode, String countryCode) { - return new TariffResponse(true, rate, hsCode, countryCode, - "Tariff rate found successfully"); - } - - public static TariffResponse notFound(String message) { - return new TariffResponse(false, null, null, null, message); - } - - public static TariffResponse error(String message) { - return new TariffResponse(false, null, null, null, message); - } - - // Getters - public boolean isSuccess() { return success; } - public BigDecimal getTariffRate() { return tariffRate; } - public String getHsCode() { return hsCode; } - public String getCountryCode() { return countryCode; } - public String getMessage() { return message; } - public String getFormattedRate() { return formattedRate; } - } +// private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TariffController2.class); +// +// private final TariffService2 tariffService; +// +// public TariffController2(TariffService2 tariffService) { +// this.tariffService = tariffService; +// } +// +// @GetMapping("/rate") +// @Operation(summary = "Zolltarif abfragen", +// description = "Ermittelt den Zolltarif für einen HS-Code und ein Herkunftsland") +// @ApiResponses(value = { +// @ApiResponse(responseCode = "200", description = "Tarif erfolgreich gefunden"), +// @ApiResponse(responseCode = "404", description = "Kein Tarif gefunden"), +// @ApiResponse(responseCode = "400", description = "Ungültige Eingabeparameter") +// }) +// public ResponseEntity getTariffRate( +// @Parameter(description = "HS-Code (6, 8 oder 10 Stellen)", example = "850410") +// @RequestParam +// @NotBlank(message = "HS Code ist erforderlich") +// @Pattern(regexp = "^[0-9]{4,10}$", message = "HS Code muss 4-10 Ziffern enthalten") +// String hsCode, +// +// @Parameter(description = "ISO-2 Ländercode", example = "CN") +// @RequestParam +// @NotBlank(message = "Ländercode ist erforderlich") +// @Pattern(regexp = "^[A-Z]{2}$", message = "Ländercode muss 2 Großbuchstaben sein") +// String countryCode) { +// +// log.info("Tariff rate request - HS Code: {}, Country: {}", hsCode, countryCode); +// +// try { +// TariffResult result = tariffService.getTariffRate(hsCode, countryCode); +// +// if (result.isFound()) { +// TariffResponse response = TariffResponse.success( +// result.getRate(), +// result.getHsCode(), +// result.getCountryCode() +// ); +// return ResponseEntity.ok(response); +// } else { +// TariffResponse response = TariffResponse.notFound(result.getMessage()); +// return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response); +// } +// +// } catch (Exception e) { +// log.error("Error getting tariff rate", e); +// TariffResponse response = TariffResponse.error("Internal server error: " + e.getMessage()); +// return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); +// } +// } +// +// @GetMapping("/health") +// @Operation(summary = "Health Check", description = "Prüft ob der Service verfügbar ist") +// public ResponseEntity> health() { +// Map response = new HashMap<>(); +// response.put("status", "UP"); +// response.put("service", "TARIC Tariff Service"); +// return ResponseEntity.ok(response); +// } +// +// /** +// * Response-Klasse für Tarif-Abfragen +// */ +// public static class TariffResponse { +// private final boolean success; +// private final BigDecimal tariffRate; +// private final String hsCode; +// private final String countryCode; +// private final String message; +// private final String formattedRate; +// +// private TariffResponse(boolean success, BigDecimal tariffRate, +// String hsCode, String countryCode, String message) { +// this.success = success; +// this.tariffRate = tariffRate; +// this.hsCode = hsCode; +// this.countryCode = countryCode; +// this.message = message; +// this.formattedRate = tariffRate != null ? +// tariffRate.toString() + " %" : null; +// } +// +// public static TariffResponse success(BigDecimal rate, String hsCode, String countryCode) { +// return new TariffResponse(true, rate, hsCode, countryCode, +// "Tariff rate found successfully"); +// } +// +// public static TariffResponse notFound(String message) { +// return new TariffResponse(false, null, null, null, message); +// } +// +// public static TariffResponse error(String message) { +// return new TariffResponse(false, null, null, null, message); +// } +// +// // Getters +// public boolean isSuccess() { return success; } +// public BigDecimal getTariffRate() { return tariffRate; } +// public String getHsCode() { return hsCode; } +// public String getCountryCode() { return countryCode; } +// public String getMessage() { return message; } +// public String getFormattedRate() { return formattedRate; } +// } } \ No newline at end of file diff --git a/src/main/java/de/avatic/taric/model/AppliedMeasure.java b/src/main/java/de/avatic/taric/model/AppliedMeasure.java index 0bb8344..c50ce70 100644 --- a/src/main/java/de/avatic/taric/model/AppliedMeasure.java +++ b/src/main/java/de/avatic/taric/model/AppliedMeasure.java @@ -3,20 +3,28 @@ package de.avatic.taric.model; import java.time.LocalDate; import java.util.Set; +import lombok.Getter; +import lombok.Setter; import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.mapping.AggregateReference; import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.MappedCollection; import jakarta.validation.constraints.NotNull; +import org.springframework.data.relational.core.mapping.Table; +@Getter +@Setter +@Table("applied_measure") public class AppliedMeasure { @Id private Integer id; + @Column("start_date") private LocalDate startDate; + @Column("end_date") private LocalDate endDate; private String amount; @@ -44,84 +52,7 @@ public class AppliedMeasure { @MappedCollection(idColumn = "applied_measure_id") private Set conditions; - public AppliedMeasure(AggregateReference measure, Set measureFootnotes, - AggregateReference legalBase, String additionalLegalBase, Set exclusions, - Set conditions, LocalDate startDate, LocalDate endDate, String amount, Integer orderNumber) { - this.measure = measure; - this.measureFootnotes = measureFootnotes; - this.legalBase = legalBase; - this.exclusions = exclusions; - this.conditions = conditions; - this.startDate = startDate; - this.endDate = endDate; - this.amount = amount; - this.orderNumber = orderNumber; - this.additionalLegalBase = additionalLegalBase; - } - public Integer getId() { - return id; - } - - public void setId(final Integer id) { - this.id = id; - } - - public LocalDate getStartDate() { - return startDate; - } - - public void setStartDate(final LocalDate startDate) { - this.startDate = startDate; - } - - public LocalDate getEndDate() { - return endDate; - } - - public void setEndDate(final LocalDate endDate) { - this.endDate = endDate; - } - - public String getAmount() { - return amount; - } - - public void setAmount(final String amount) { - this.amount = amount; - } - - public AggregateReference getMeasure() { - return measure; - } - - public void setMeasure(final AggregateReference measure) { - this.measure = measure; - } - - public AggregateReference getLegalBase() { - return legalBase; - } - - public void setLegalBase(final AggregateReference legalBase) { - this.legalBase = legalBase; - } - - public Integer getOrderNumber() { - return orderNumber; - } - - public Set getMeasureFootnotes() { - return measureFootnotes; - } - - public Set getExclusions() { - return exclusions; - } - - public Set getConditions() { - return conditions; - } } diff --git a/src/main/java/de/avatic/taric/model/AppliedMeasureCondition.java b/src/main/java/de/avatic/taric/model/AppliedMeasureCondition.java index 589b399..5fe3d41 100644 --- a/src/main/java/de/avatic/taric/model/AppliedMeasureCondition.java +++ b/src/main/java/de/avatic/taric/model/AppliedMeasureCondition.java @@ -1,12 +1,17 @@ package de.avatic.taric.model; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.mapping.AggregateReference; import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.Table; import org.springframework.lang.Nullable; -import jakarta.validation.constraints.NotNull; - +@Getter +@Setter +@Table("applied_measure_condition") public class AppliedMeasureCondition { @Id @@ -37,82 +42,6 @@ public class AppliedMeasureCondition { @Column("condition_type_id") private AggregateReference conditionType; - public AppliedMeasureCondition(AggregateReference measureAction, - AggregateReference monetaryUnit, - AggregateReference unit, - AggregateReference certificate, - AggregateReference conditionType, String amount, Integer squenceNo) { - this.measureAction = measureAction; - this.monetaryUnit = monetaryUnit; - this.unit = unit; - this.certificate = certificate; - this.conditionType = conditionType; - this.amount = amount; - this.sequenceNo = squenceNo; - } - public Integer getId() { - return id; - } - - public void setId(final Integer id) { - this.id = id; - } - - public Integer getSequenceNo() { - return sequenceNo; - } - - public void setSequenceNo(final Integer sequenceNo) { - this.sequenceNo = sequenceNo; - } - - public String getAmount() { - return amount; - } - - public void setAmount(final String amount) { - this.amount = amount; - } - - public AggregateReference getMeasureAction() { - return measureAction; - } - - public void setMeasureAction(final AggregateReference measureAction) { - this.measureAction = measureAction; - } - - public AggregateReference getMonetaryUnit() { - return monetaryUnit; - } - - public void setMonetaryUnit(final AggregateReference monetaryUnit) { - this.monetaryUnit = monetaryUnit; - } - - public AggregateReference getUnit() { - return unit; - } - - public void setUnit(final AggregateReference unit) { - this.unit = unit; - } - - public AggregateReference getCertificate() { - return certificate; - } - - public void setCertificate(final AggregateReference certificate) { - this.certificate = certificate; - } - - public AggregateReference getConditionType() { - return conditionType; - } - - public void setConditionType(final AggregateReference conditionType) { - this.conditionType = conditionType; - } } diff --git a/src/main/java/de/avatic/taric/model/Import.java b/src/main/java/de/avatic/taric/model/Import.java index 6ce1244..4804649 100644 --- a/src/main/java/de/avatic/taric/model/Import.java +++ b/src/main/java/de/avatic/taric/model/Import.java @@ -2,6 +2,8 @@ package de.avatic.taric.model; import java.util.Set; +import lombok.Getter; +import lombok.Setter; import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.mapping.AggregateReference; import org.springframework.data.relational.core.mapping.Column; @@ -10,6 +12,8 @@ import org.springframework.data.relational.core.mapping.Table; import jakarta.validation.constraints.NotNull; +@Setter +@Getter @Table("import") public class Import { @@ -18,70 +22,19 @@ public class Import { @NotNull @Column("nomenclature_id") - private AggregateReference nomenclature; + private Integer nomenclature; @Column("additional_code_id") - private AggregateReference additionalCode; + private Integer additionalCode; @Column("geo_id") private AggregateReference geo; @Column("geo_group_id") - private AggregateReference geoGroup; + private AggregateReference geoGroup; @MappedCollection(idColumn = "import_id") Set appliedMeasures; - - public Import(AggregateReference nomenclature, - AggregateReference additionalCode, Set appliedMeasures, AggregateReference geo, - AggregateReference geoGroup) { - this.nomenclature = nomenclature; - this.additionalCode = additionalCode; - this.appliedMeasures = appliedMeasures; - this.geo = geo; - this.geoGroup = geoGroup; - } - - public Integer getId() { - return id; - } - - public void setId(final Integer id) { - this.id = id; - } - - public AggregateReference getNomenclature() { - return nomenclature; - } - - public void setNomenclature(final AggregateReference nomenclature) { - this.nomenclature = nomenclature; - } - - public AggregateReference getAdditionalCode() { - return additionalCode; - } - - public void setAdditionalCode(final AggregateReference additionalCode) { - this.additionalCode = additionalCode; - } - - public AggregateReference getGeo() { - return geo; - } - - public void setGeo(final AggregateReference geo) { - this.geo = geo; - } - - public AggregateReference getGeoGroup() { - return geoGroup; - } - - public void setGeoGroup(final AggregateReference geoGroup) { - this.geoGroup = geoGroup; - } - } diff --git a/src/main/java/de/avatic/taric/model/Measure.java b/src/main/java/de/avatic/taric/model/Measure.java index b2c9fbb..f6ad4c9 100644 --- a/src/main/java/de/avatic/taric/model/Measure.java +++ b/src/main/java/de/avatic/taric/model/Measure.java @@ -28,6 +28,7 @@ public class Measure { private Integer tmCode; + @Column("start_date") private LocalDate startDate; @MappedCollection(idColumn = "ref_id") diff --git a/src/main/java/de/avatic/taric/model/MeasureExclusion.java b/src/main/java/de/avatic/taric/model/MeasureExclusion.java index e782bf3..867b76a 100644 --- a/src/main/java/de/avatic/taric/model/MeasureExclusion.java +++ b/src/main/java/de/avatic/taric/model/MeasureExclusion.java @@ -7,20 +7,24 @@ import org.springframework.data.jdbc.core.mapping.AggregateReference; import org.springframework.data.relational.core.mapping.Column; import jakarta.validation.constraints.NotNull; +import org.springframework.data.relational.core.mapping.Table; import java.time.LocalDate; @Getter @Setter +@Table("measure_exclusion") public class MeasureExclusion { @Id private Integer id; - LocalDate startDate; + @Column("start_date") + private LocalDate startDate; - LocalDate endDate; + @Column("end_date") + private LocalDate endDate; @NotNull @Column("geo_id") diff --git a/src/main/java/de/avatic/taric/model/MeasureFootnote.java b/src/main/java/de/avatic/taric/model/MeasureFootnote.java index fbc3489..5059fba 100644 --- a/src/main/java/de/avatic/taric/model/MeasureFootnote.java +++ b/src/main/java/de/avatic/taric/model/MeasureFootnote.java @@ -6,21 +6,26 @@ import lombok.Setter; import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.mapping.AggregateReference; import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.Table; import java.time.LocalDate; @Getter @Setter +@Table("measure_footnote") public class MeasureFootnote { @Id private Integer id; - LocalDate startDate; - LocalDate endDate; + @Column("start_date") + private LocalDate startDate; + + @Column("end_date") + private LocalDate endDate; @Column("footnote_id") - private AggregateReference footnote; + private Integer footnote; diff --git a/src/main/java/de/avatic/taric/model/Nomenclature.java b/src/main/java/de/avatic/taric/model/Nomenclature.java index c3c7ab3..e58fcd2 100644 --- a/src/main/java/de/avatic/taric/model/Nomenclature.java +++ b/src/main/java/de/avatic/taric/model/Nomenclature.java @@ -9,6 +9,7 @@ import lombok.Getter; import lombok.Setter; import org.springframework.data.annotation.Id; import org.springframework.data.relational.core.mapping.MappedCollection; +import org.springframework.data.relational.core.mapping.Table; import java.time.LocalDate; import java.util.Set; @@ -16,6 +17,7 @@ import java.util.Set; @Getter @Setter +@Table("nomenclature") public class Nomenclature { @Id diff --git a/src/main/java/de/avatic/taric/repository/ImportRepository.java b/src/main/java/de/avatic/taric/repository/ImportRepository.java index b673107..3446e0f 100644 --- a/src/main/java/de/avatic/taric/repository/ImportRepository.java +++ b/src/main/java/de/avatic/taric/repository/ImportRepository.java @@ -1,7 +1,6 @@ package de.avatic.taric.repository; import de.avatic.taric.model.Import; -import org.springframework.data.jdbc.repository.query.Query; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; import java.util.List; @@ -9,9 +8,8 @@ import java.util.List; @Repository public interface ImportRepository extends CrudRepository { - @Query("SELECT * FROM import WHERE nomenclature_id = :nomenclatureId AND geo_id = :geoId") - List findByNomenclatureIdAndGeoId(Integer nomenclatureId, Integer geoId); + List findByNomenclatureAndGeo(Integer nomenclatureId, Integer geoId); + + List findByNomenclatureAndGeoGroupIn(Integer nomenclatureId, List geoGroupIds); - @Query("SELECT * FROM import WHERE nomenclature_id = :nomenclatureId AND geo_group_id = :geoGroupId") - List findByNomenclatureIdAndGeoGroupId(Integer nomenclatureId, Integer geoGroupId); } diff --git a/src/main/java/de/avatic/taric/service/NomenclatureService.java b/src/main/java/de/avatic/taric/service/NomenclatureService.java index fcf3cdb..03d10d0 100644 --- a/src/main/java/de/avatic/taric/service/NomenclatureService.java +++ b/src/main/java/de/avatic/taric/service/NomenclatureService.java @@ -4,10 +4,7 @@ import de.avatic.taric.model.Nomenclature; import de.avatic.taric.repository.NomenclatureRepository; import org.springframework.stereotype.Service; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; +import java.util.*; @Service public class NomenclatureService { @@ -19,14 +16,13 @@ public class NomenclatureService { } - public Optional getNomenclature(String hscode) { return nomenclatureRepository.findByHscode(normalize(hscode.replaceAll("[^0-9]", ""))); } public boolean isDeclarable(String hscode) { var nomenclature = getNomenclature(hscode); - if(nomenclature.isEmpty()) return false; + if (nomenclature.isEmpty()) return false; return nomenclature.get().getIsLeaf(); } @@ -40,17 +36,22 @@ public class NomenclatureService { if (hscode == null) return Collections.emptyList(); List cascade = getCascade(normalize(hscode.replaceAll("[^0-9]", ""))); - var cascadeResp = cascade.stream().map(nomenclatureRepository::findByHscode).toList(); - return cascade.stream().map(nomenclatureRepository::findByHscode).flatMap(Optional::stream).toList(); } private List getCascade(String hscode) { + var set = new HashSet(); var parents = new ArrayList(); var hierarchyLevel = getHierarchyLevel(hscode); while (hierarchyLevel > 0) { - parents.add(getParent(hscode, hierarchyLevel)); + var parent = getParent(hscode, hierarchyLevel); + + if (!set.contains(parent)) { + set.add(parent); + parents.add(parent); + } + hierarchyLevel -= 2; } @@ -64,14 +65,14 @@ public class NomenclatureService { private int getHierarchyLevel(String hscode) { int idx = hscode.length(); - while(idx > 0) { + while (idx > 0) { idx--; if (hscode.charAt(idx) != '0') { break; } } - return (idx+1) % 2 == 0 ? idx + 1 : idx + 2; + return (idx + 1) % 2 == 0 ? idx + 1 : idx + 2; } private String normalize(String hscode) { diff --git a/src/main/java/de/avatic/taric/service/TariffService.java b/src/main/java/de/avatic/taric/service/TariffService.java index 716b7c3..10f42d1 100644 --- a/src/main/java/de/avatic/taric/service/TariffService.java +++ b/src/main/java/de/avatic/taric/service/TariffService.java @@ -2,21 +2,32 @@ package de.avatic.taric.service; import de.avatic.taric.error.ArgumentException; +import de.avatic.taric.model.AppliedMeasure; +import de.avatic.taric.model.Geo; +import de.avatic.taric.model.GeoGroup; import de.avatic.taric.model.Nomenclature; +import de.avatic.taric.repository.ImportRepository; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import java.util.*; +import java.util.stream.Collectors; + @Service +@Slf4j public class TariffService { private final GeoService geoService; private final NomenclatureService nomenclatureService; + private final ImportRepository importRepository; - public TariffService(GeoService geoService, NomenclatureService nomenclatureService) { + public TariffService(GeoService geoService, NomenclatureService nomenclatureService, ImportRepository importRepository) { this.geoService = geoService; this.nomenclatureService = nomenclatureService; + this.importRepository = importRepository; } - public void importTariffs(String hsCode, String countryCode) { + public Optional importTariffs(String hsCode, String countryCode) { var geoGroups = geoService.getGeoGroupByCountryCode(countryCode); var geo = geoService.getGeo(countryCode); @@ -24,12 +35,84 @@ public class TariffService { var nomenclature = nomenclatureService.getNomenclature(hsCode); var cascade = nomenclatureService.getNomenclatureCascade(hsCode); - if(nomenclature.isEmpty() || !nomenclature.get().getIsLeaf()) throw new ArgumentException("hsCode"); - if(geo.isEmpty()) throw new ArgumentException("countryCode"); + if (nomenclature.isEmpty() || !nomenclature.get().getIsLeaf()) throw new ArgumentException("hsCode"); + if (geo.isEmpty()) throw new ArgumentException("countryCode"); + for (Nomenclature n : cascade) { + var applMeasures = findAppliedMeasureByGeo(n, geo.get()); + if (!applMeasures.isEmpty()) { + var tariff = findTariff(applMeasures); + + if (tariff.isPresent()) + return tariff; + + } + + } + + for (Nomenclature n : cascade) { + var applMeasures = findAppliedMeasureByGeoGroup(n, geo.get(), geoGroups); + + if (!applMeasures.isEmpty()) { + var tariff = findTariff(applMeasures); + + if (tariff.isPresent()) + return tariff; + } + } + + return Optional.empty(); } + private Optional findTariff(Collection measures) { + + List percentages = measures.stream().map(AppliedMeasure::getAmount) + .filter(str -> str != null && str.trim().matches("\\d+\\.\\d+\\s*%")) + .map(str -> Double.parseDouble(str.trim().replace("%", "").trim())/100) + .toList(); + + if (!percentages.isEmpty()) return Optional.of(percentages.getFirst()); + return Optional.empty(); + } + + public Collection findAppliedMeasureByGeo(Nomenclature nomenclature, Geo geo) { + + var foundImport = importRepository.findByNomenclatureAndGeo(nomenclature.getId(), geo.getId()); + var applMeasures = new ArrayList(); + + if (foundImport.isEmpty()) return Collections.emptyList(); + + for (var imp : foundImport) { + for (var applMeasure : imp.getAppliedMeasures()) { + if (applMeasure.getExclusions().stream().noneMatch(ex -> ex.getGeo().getId().equals(geo.getId()))) { + applMeasures.add(applMeasure); + } + } + } + + return applMeasures; + + } + + public Collection findAppliedMeasureByGeoGroup(Nomenclature nomenclature, Geo geo, List geoGroups) { + var foundImport = importRepository.findByNomenclatureAndGeoGroupIn(nomenclature.getId(), geoGroups.stream().map(GeoGroup::getId).toList()); + var applMeasures = new ArrayList(); + + if (foundImport.isEmpty()) return Collections.emptyList(); + + for (var imp : foundImport) { + for (var applMeasure : imp.getAppliedMeasures()) { + if (applMeasure.getExclusions().stream().noneMatch(ex -> ex.getGeo().getId().equals(geo.getId()))) { + applMeasures.add(applMeasure); + } + } + } + + return applMeasures; + } + + } diff --git a/src/main/java/de/avatic/taric/service/TariffService2.java b/src/main/java/de/avatic/taric/service/TariffService2.java index 8124dab..8fd9975 100644 --- a/src/main/java/de/avatic/taric/service/TariffService2.java +++ b/src/main/java/de/avatic/taric/service/TariffService2.java @@ -37,267 +37,267 @@ public class TariffService2 { this.measureExclusionRepository = measureExclusionRepository; } - /** - * Ermittelt den Zolltarif für einen HS-Code und ein Herkunftsland - * - * @param hsCode HS-Code (kann 6, 8 oder 10 Stellen haben) - * @param countryCode ISO-2 Ländercode (z.B. "CN" für China) - * @return Zolltarif in Prozent - */ - @Transactional(readOnly = true) - public TariffResult getTariffRate(String hsCode, String countryCode) { - log.info("Getting tariff rate for HS Code: {} from country: {}", hsCode, countryCode); - - // Normalisiere HS-Code auf 10 Stellen - String normalizedHsCode = normalizeHsCode(hsCode); - - // Finde alle relevanten Nomenclature-Codes (inkl. Parent-Codes durch Cascade-Prinzip) - List relevantCodes = findRelevantNomenclatureCodes(normalizedHsCode); - log.debug("Found relevant codes: {}", relevantCodes); - - // Hole das Land - Optional countryOpt = geoRepository.findByIso3166Code(countryCode); - if (countryOpt.isEmpty()) { - log.warn("Country not found: {}", countryCode); - return TariffResult.notFound("Country code not found: " + countryCode); - } - - Geo country = countryOpt.get(); - - // Finde alle Imports für die relevanten Codes - List imports = findRelevantImports(relevantCodes, country); - - if (imports.isEmpty()) { - log.info("No imports found for codes: {} and country: {}", relevantCodes, countryCode); - // Versuche Erga Omnes (alle Länder) - imports = findErgaOmnesImports(relevantCodes); - } - - if (imports.isEmpty()) { - return TariffResult.notFound("No tariff data found for HS code: " + hsCode); - } - - // Finde die anwendbaren Maßnahmen - BigDecimal tariffRate = calculateTariffRate(imports, country); - - return TariffResult.success(tariffRate, normalizedHsCode, countryCode); - } - - private String normalizeHsCode(String hsCode) { - // Entferne alle nicht-numerischen Zeichen - String cleaned = hsCode.replaceAll("[^0-9]", ""); - - // Fülle auf 10 Stellen mit Nullen auf - while (cleaned.length() < 10) { - cleaned += "0"; - } - - // Begrenze auf 10 Stellen - if (cleaned.length() > 10) { - cleaned = cleaned.substring(0, 10); - } - - return cleaned; - } - - private List findRelevantNomenclatureCodes(String hsCode) { - List codes = new ArrayList<>(); - codes.add(hsCode); - - // Füge Parent-Codes hinzu (Cascade-Prinzip) - // Beispiel: 8504101010 -> auch 85041010, 850410, 8504, 85 - String code = hsCode; - while (code.length() > 2) { - // Entferne die letzten 2 Nullen - if (code.endsWith("00")) { - code = code.substring(0, code.length() - 2); - codes.add(code + "0".repeat(10 - code.length())); - } else { - break; - } - } - - return codes; - } - - private List findRelevantImports(List nomenclatureCodes, Geo country) { - List imports = new ArrayList<>(); - - for (String code : nomenclatureCodes) { - // Suche direkte Länder-Zuordnungen - Optional nomenclatureOpt = nomenclatureRepository.findByHscode(code); - if (nomenclatureOpt.isPresent()) { - Nomenclature nomenclature = nomenclatureOpt.get(); - - // Suche Imports mit direkter Geo-Zuordnung - imports.addAll(importRepository.findByNomenclatureIdAndGeoId( - nomenclature.getId(), country.getId())); - - // Suche auch nach Ländergruppen-Mitgliedschaften - List memberships = - geoGroupMembershipRepository.findByGeoId(country.getId()); - - for (GeoGroupMembership membership : memberships) { - // Hole die geo_group_id aus der membership - Integer geoGroupId = geoGroupMembershipRepository - .findGeoGroupIdByMembershipId(membership.getId()); - if (geoGroupId != null) { - imports.addAll(importRepository.findByNomenclatureIdAndGeoGroupId( - nomenclature.getId(), geoGroupId)); - } - } - } - } - - return imports; - } - - private List findErgaOmnesImports(List nomenclatureCodes) { - List imports = new ArrayList<>(); - - // Erga Omnes hat normalerweise den Code "1011" - Optional ergaOmnes = geoGroupRepository.findByGeoGroupCode("1011"); - if (ergaOmnes.isEmpty()) { - return imports; - } - - for (String code : nomenclatureCodes) { - Optional nomenclatureOpt = nomenclatureRepository.findByHscode(code); - if (nomenclatureOpt.isPresent()) { - imports.addAll(importRepository.findByNomenclatureIdAndGeoGroupId( - nomenclatureOpt.get().getId(), ergaOmnes.get().getId())); - } - } - - return imports; - } - - private BigDecimal calculateTariffRate(List imports, Geo country) { - BigDecimal lowestRate = null; - LocalDate today = LocalDate.now(); - - for (Import imp : imports) { - // Nutze die korrekte Repository-Methode mit Import-Reference - List measures = appliedMeasureRepository - .findByImport(AggregateReference.to(imp.getId())); - - for (AppliedMeasure appliedMeasure : measures) { - // Prüfe ob Maßnahme gültig ist - if (!isMeasureValid(appliedMeasure, today)) { - continue; - } - - // Prüfe ob das Land ausgeschlossen ist - if (isCountryExcluded(appliedMeasure, country)) { - continue; - } - - // Hole die Maßnahme über die AggregateReference - if (appliedMeasure.getMeasure() == null) { - continue; - } - - Optional measureOpt = measureRepository.findById( - appliedMeasure.getMeasure().getId()); - if (measureOpt.isEmpty()) { - continue; - } - - Measure measure = measureOpt.get(); - - // Wir interessieren uns hauptsächlich für Third Country Duty (103) - // und Preferential Tariff (142, 143) - if (!"103".equals(measure.getMeasureCode()) && - !"142".equals(measure.getMeasureCode()) && - !"143".equals(measure.getMeasureCode())) { - continue; - } - - // Parse den Zollsatz aus dem amount String - BigDecimal rate = parseTariffRate(appliedMeasure.getAmount()); - if (rate != null && (lowestRate == null || rate.compareTo(lowestRate) < 0)) { - lowestRate = rate; - } - } - } - - return lowestRate != null ? lowestRate : BigDecimal.ZERO; - } - - private boolean isMeasureValid(AppliedMeasure measure, LocalDate date) { - if (measure.getStartDate() != null && measure.getStartDate().isAfter(date)) { - return false; - } - if (measure.getEndDate() != null && measure.getEndDate().isBefore(date)) { - return false; - } - return true; - } - - private boolean isCountryExcluded(AppliedMeasure measure, Geo country) { - // Finde Exclusions für diese AppliedMeasure - List exclusions = - measureExclusionRepository.findByAppliedMeasure( - AggregateReference.to(measure.getId())); - - return exclusions.stream() - .anyMatch(exc -> exc.getGeo() != null && - exc.getGeo().getId().equals(country.getId())); - } - - private BigDecimal parseTariffRate(String amount) { - if (amount == null || amount.isEmpty()) { - return null; - } - - // Einfacher Parser für Prozentsätze - // Format: "12.5 %" oder "12.5% + ..." oder "12.5 % MAX ..." - Pattern pattern = Pattern.compile("^([0-9]+\\.?[0-9]*)\\s*%"); - Matcher matcher = pattern.matcher(amount); - - if (matcher.find()) { - try { - return new BigDecimal(matcher.group(1)); - } catch (NumberFormatException e) { - log.warn("Could not parse tariff rate from: {}", amount); - } - } - - return null; - } - - /** - * Result-Klasse für Tarif-Abfragen - */ - public static class TariffResult { - private final boolean found; - private final BigDecimal rate; - private final String hsCode; - private final String countryCode; - private final String message; - - private TariffResult(boolean found, BigDecimal rate, String hsCode, - String countryCode, String message) { - this.found = found; - this.rate = rate; - this.hsCode = hsCode; - this.countryCode = countryCode; - this.message = message; - } - - public static TariffResult success(BigDecimal rate, String hsCode, String countryCode) { - return new TariffResult(true, rate, hsCode, countryCode, null); - } - - public static TariffResult notFound(String message) { - return new TariffResult(false, null, null, null, message); - } - - // Getters - public boolean isFound() { return found; } - public BigDecimal getRate() { return rate; } - public String getHsCode() { return hsCode; } - public String getCountryCode() { return countryCode; } - public String getMessage() { return message; } - } +// /** +// * Ermittelt den Zolltarif für einen HS-Code und ein Herkunftsland +// * +// * @param hsCode HS-Code (kann 6, 8 oder 10 Stellen haben) +// * @param countryCode ISO-2 Ländercode (z.B. "CN" für China) +// * @return Zolltarif in Prozent +// */ +// @Transactional(readOnly = true) +// public TariffResult getTariffRate(String hsCode, String countryCode) { +// log.info("Getting tariff rate for HS Code: {} from country: {}", hsCode, countryCode); +// +// // Normalisiere HS-Code auf 10 Stellen +// String normalizedHsCode = normalizeHsCode(hsCode); +// +// // Finde alle relevanten Nomenclature-Codes (inkl. Parent-Codes durch Cascade-Prinzip) +// List relevantCodes = findRelevantNomenclatureCodes(normalizedHsCode); +// log.debug("Found relevant codes: {}", relevantCodes); +// +// // Hole das Land +// Optional countryOpt = geoRepository.findByIso3166Code(countryCode); +// if (countryOpt.isEmpty()) { +// log.warn("Country not found: {}", countryCode); +// return TariffResult.notFound("Country code not found: " + countryCode); +// } +// +// Geo country = countryOpt.get(); +// +// // Finde alle Imports für die relevanten Codes +// List imports = findRelevantImports(relevantCodes, country); +// +// if (imports.isEmpty()) { +// log.info("No imports found for codes: {} and country: {}", relevantCodes, countryCode); +// // Versuche Erga Omnes (alle Länder) +// imports = findErgaOmnesImports(relevantCodes); +// } +// +// if (imports.isEmpty()) { +// return TariffResult.notFound("No tariff data found for HS code: " + hsCode); +// } +// +// // Finde die anwendbaren Maßnahmen +// BigDecimal tariffRate = calculateTariffRate(imports, country); +// +// return TariffResult.success(tariffRate, normalizedHsCode, countryCode); +// } +// +// private String normalizeHsCode(String hsCode) { +// // Entferne alle nicht-numerischen Zeichen +// String cleaned = hsCode.replaceAll("[^0-9]", ""); +// +// // Fülle auf 10 Stellen mit Nullen auf +// while (cleaned.length() < 10) { +// cleaned += "0"; +// } +// +// // Begrenze auf 10 Stellen +// if (cleaned.length() > 10) { +// cleaned = cleaned.substring(0, 10); +// } +// +// return cleaned; +// } +// +// private List findRelevantNomenclatureCodes(String hsCode) { +// List codes = new ArrayList<>(); +// codes.add(hsCode); +// +// // Füge Parent-Codes hinzu (Cascade-Prinzip) +// // Beispiel: 8504101010 -> auch 85041010, 850410, 8504, 85 +// String code = hsCode; +// while (code.length() > 2) { +// // Entferne die letzten 2 Nullen +// if (code.endsWith("00")) { +// code = code.substring(0, code.length() - 2); +// codes.add(code + "0".repeat(10 - code.length())); +// } else { +// break; +// } +// } +// +// return codes; +// } +// +// private List findRelevantImports(List nomenclatureCodes, Geo country) { +// List imports = new ArrayList<>(); +// +// for (String code : nomenclatureCodes) { +// // Suche direkte Länder-Zuordnungen +// Optional nomenclatureOpt = nomenclatureRepository.findByHscode(code); +// if (nomenclatureOpt.isPresent()) { +// Nomenclature nomenclature = nomenclatureOpt.get(); +// +// // Suche Imports mit direkter Geo-Zuordnung +// imports.addAll(importRepository.findByNomenclatureIdAndGeoId( +// nomenclature.getId(), country.getId())); +// +// // Suche auch nach Ländergruppen-Mitgliedschaften +// List memberships = +// geoGroupMembershipRepository.findByGeoId(country.getId()); +// +// for (GeoGroupMembership membership : memberships) { +// // Hole die geo_group_id aus der membership +// Integer geoGroupId = geoGroupMembershipRepository +// .findGeoGroupIdByMembershipId(membership.getId()); +// if (geoGroupId != null) { +// imports.addAll(importRepository.findByNomenclatureIdAndGeoGroupId( +// nomenclature.getId(), geoGroupId)); +// } +// } +// } +// } +// +// return imports; +// } +// +// private List findErgaOmnesImports(List nomenclatureCodes) { +// List imports = new ArrayList<>(); +// +// // Erga Omnes hat normalerweise den Code "1011" +// Optional ergaOmnes = geoGroupRepository.findByGeoGroupCode("1011"); +// if (ergaOmnes.isEmpty()) { +// return imports; +// } +// +// for (String code : nomenclatureCodes) { +// Optional nomenclatureOpt = nomenclatureRepository.findByHscode(code); +// if (nomenclatureOpt.isPresent()) { +// imports.addAll(importRepository.findByNomenclatureIdAndGeoGroupId( +// nomenclatureOpt.get().getId(), ergaOmnes.get().getId())); +// } +// } +// +// return imports; +// } +// +// private BigDecimal calculateTariffRate(List imports, Geo country) { +// BigDecimal lowestRate = null; +// LocalDate today = LocalDate.now(); +// +// for (Import imp : imports) { +// // Nutze die korrekte Repository-Methode mit Import-Reference +// List measures = appliedMeasureRepository +// .findByImport(AggregateReference.to(imp.getId())); +// +// for (AppliedMeasure appliedMeasure : measures) { +// // Prüfe ob Maßnahme gültig ist +// if (!isMeasureValid(appliedMeasure, today)) { +// continue; +// } +// +// // Prüfe ob das Land ausgeschlossen ist +// if (isCountryExcluded(appliedMeasure, country)) { +// continue; +// } +// +// // Hole die Maßnahme über die AggregateReference +// if (appliedMeasure.getMeasure() == null) { +// continue; +// } +// +// Optional measureOpt = measureRepository.findById( +// appliedMeasure.getMeasure().getId()); +// if (measureOpt.isEmpty()) { +// continue; +// } +// +// Measure measure = measureOpt.get(); +// +// // Wir interessieren uns hauptsächlich für Third Country Duty (103) +// // und Preferential Tariff (142, 143) +// if (!"103".equals(measure.getMeasureCode()) && +// !"142".equals(measure.getMeasureCode()) && +// !"143".equals(measure.getMeasureCode())) { +// continue; +// } +// +// // Parse den Zollsatz aus dem amount String +// BigDecimal rate = parseTariffRate(appliedMeasure.getAmount()); +// if (rate != null && (lowestRate == null || rate.compareTo(lowestRate) < 0)) { +// lowestRate = rate; +// } +// } +// } +// +// return lowestRate != null ? lowestRate : BigDecimal.ZERO; +// } +// +// private boolean isMeasureValid(AppliedMeasure measure, LocalDate date) { +// if (measure.getStartDate() != null && measure.getStartDate().isAfter(date)) { +// return false; +// } +// if (measure.getEndDate() != null && measure.getEndDate().isBefore(date)) { +// return false; +// } +// return true; +// } +// +// private boolean isCountryExcluded(AppliedMeasure measure, Geo country) { +// // Finde Exclusions für diese AppliedMeasure +// List exclusions = +// measureExclusionRepository.findByAppliedMeasure( +// AggregateReference.to(measure.getId())); +// +// return exclusions.stream() +// .anyMatch(exc -> exc.getGeo() != null && +// exc.getGeo().getId().equals(country.getId())); +// } +// +// private BigDecimal parseTariffRate(String amount) { +// if (amount == null || amount.isEmpty()) { +// return null; +// } +// +// // Einfacher Parser für Prozentsätze +// // Format: "12.5 %" oder "12.5% + ..." oder "12.5 % MAX ..." +// Pattern pattern = Pattern.compile("^([0-9]+\\.?[0-9]*)\\s*%"); +// Matcher matcher = pattern.matcher(amount); +// +// if (matcher.find()) { +// try { +// return new BigDecimal(matcher.group(1)); +// } catch (NumberFormatException e) { +// log.warn("Could not parse tariff rate from: {}", amount); +// } +// } +// +// return null; +// } +// +// /** +// * Result-Klasse für Tarif-Abfragen +// */ +// public static class TariffResult { +// private final boolean found; +// private final BigDecimal rate; +// private final String hsCode; +// private final String countryCode; +// private final String message; +// +// private TariffResult(boolean found, BigDecimal rate, String hsCode, +// String countryCode, String message) { +// this.found = found; +// this.rate = rate; +// this.hsCode = hsCode; +// this.countryCode = countryCode; +// this.message = message; +// } +// +// public static TariffResult success(BigDecimal rate, String hsCode, String countryCode) { +// return new TariffResult(true, rate, hsCode, countryCode, null); +// } +// +// public static TariffResult notFound(String message) { +// return new TariffResult(false, null, null, null, message); +// } +// +// // Getters +// public boolean isFound() { return found; } +// public BigDecimal getRate() { return rate; } +// public String getHsCode() { return hsCode; } +// public String getCountryCode() { return countryCode; } +// public String getMessage() { return message; } +// } } \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 83d9558..75e85ce 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,2 @@ spring.application.name=taric +logging.level.org.springframework.data.jdbc.core.convert.RowDocumentResultSetExtractor=ERROR \ No newline at end of file diff --git a/src/main/resources/db/migration/V1__Create_schema.sql b/src/main/resources/db/migration/V1__Create_schema.sql index ebf65c3..0170d8e 100644 --- a/src/main/resources/db/migration/V1__Create_schema.sql +++ b/src/main/resources/db/migration/V1__Create_schema.sql @@ -73,7 +73,8 @@ create table if not exists `measure` create table if not exists `measure_action` ( id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, - measure_action_code CHAR(2) + measure_action_code CHAR(2), + CONSTRAINT UC_MeasureAction UNIQUE (measure_action_code) ); -- geo @@ -82,7 +83,8 @@ create table if not exists `geo` id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, iso_3166_code CHAR(2), start_date DATE, - end_date DATE + end_date DATE, + CONSTRAINT UC_IsoCode UNIQUE (iso_3166_code) ); create table if not exists `geo_group` @@ -91,7 +93,8 @@ create table if not exists `geo_group` geo_group_code CHAR(4), abbr TEXT, start_date DATE, - end_date DATE + end_date DATE, + CONSTRAINT UC_GroupCode UNIQUE (geo_group_code) ); create table if not exists `geo_group_membership` @@ -102,7 +105,8 @@ create table if not exists `geo_group_membership` start_date DATE, end_date DATE, FOREIGN KEY (geo_id) REFERENCES geo(id), - FOREIGN KEY (geo_group_id) REFERENCES geo_group(id) + FOREIGN KEY (geo_group_id) REFERENCES geo_group(id), + CONSTRAINT UC_GeoGroupTuple UNIQUE (geo_id, geo_group_id) ); -- nomenclature @@ -134,7 +138,8 @@ create table if not exists `additional_code` id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, additional_code CHAR(4), start_date DATE, - end_date DATE + end_date DATE, + CONSTRAINT UC_AdditionalCode UNIQUE (additional_code) ); -- import, applied_measures