Update Java version to 23 and enhance tariff services with improved repository integration, optional return types, and unique constraints in the schema.

This commit is contained in:
Jan 2025-11-02 21:42:30 +01:00
parent 10cc6bc5f0
commit ce79b20808
16 changed files with 525 additions and 613 deletions

View file

@ -27,7 +27,7 @@
<url/> <url/>
</scm> </scm>
<properties> <properties>
<java.version>17</java.version> <java.version>23</java.version>
</properties> </properties>
<dependencies> <dependencies>
<dependency> <dependency>

View file

@ -1,6 +1,7 @@
package de.avatic.taric.controller; package de.avatic.taric.controller;
import de.avatic.taric.service.TariffService; import de.avatic.taric.service.TariffService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
@ -18,10 +19,8 @@ public class TariffController {
} }
@GetMapping("") @GetMapping("")
public void getTariffRate(@RequestParam String hsCode, @RequestParam String countryCode) { public ResponseEntity<Double> getTariffRate(@RequestParam String hsCode, @RequestParam String countryCode) {
return tariffService.importTariffs( hsCode, countryCode).map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build());
tariffService.importTariffs( hsCode, countryCode);
} }
} }

View file

@ -1,7 +1,7 @@
package de.avatic.taric.controller; package de.avatic.taric.controller;
import de.avatic.taric.service.TariffService2; 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.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse; 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") @Tag(name = "Tariff API", description = "API zur Abfrage von Zolltarifen")
public class TariffController2 { public class TariffController2 {
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TariffController2.class); // private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TariffController2.class);
//
private final TariffService2 tariffService; // private final TariffService2 tariffService;
//
public TariffController2(TariffService2 tariffService) { // public TariffController2(TariffService2 tariffService) {
this.tariffService = tariffService; // this.tariffService = tariffService;
} // }
//
@GetMapping("/rate") // @GetMapping("/rate")
@Operation(summary = "Zolltarif abfragen", // @Operation(summary = "Zolltarif abfragen",
description = "Ermittelt den Zolltarif für einen HS-Code und ein Herkunftsland") // description = "Ermittelt den Zolltarif für einen HS-Code und ein Herkunftsland")
@ApiResponses(value = { // @ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Tarif erfolgreich gefunden"), // @ApiResponse(responseCode = "200", description = "Tarif erfolgreich gefunden"),
@ApiResponse(responseCode = "404", description = "Kein Tarif gefunden"), // @ApiResponse(responseCode = "404", description = "Kein Tarif gefunden"),
@ApiResponse(responseCode = "400", description = "Ungültige Eingabeparameter") // @ApiResponse(responseCode = "400", description = "Ungültige Eingabeparameter")
}) // })
public ResponseEntity<TariffResponse> getTariffRate( // public ResponseEntity<TariffResponse> getTariffRate(
@Parameter(description = "HS-Code (6, 8 oder 10 Stellen)", example = "850410") // @Parameter(description = "HS-Code (6, 8 oder 10 Stellen)", example = "850410")
@RequestParam // @RequestParam
@NotBlank(message = "HS Code ist erforderlich") // @NotBlank(message = "HS Code ist erforderlich")
@Pattern(regexp = "^[0-9]{4,10}$", message = "HS Code muss 4-10 Ziffern enthalten") // @Pattern(regexp = "^[0-9]{4,10}$", message = "HS Code muss 4-10 Ziffern enthalten")
String hsCode, // String hsCode,
//
@Parameter(description = "ISO-2 Ländercode", example = "CN") // @Parameter(description = "ISO-2 Ländercode", example = "CN")
@RequestParam // @RequestParam
@NotBlank(message = "Ländercode ist erforderlich") // @NotBlank(message = "Ländercode ist erforderlich")
@Pattern(regexp = "^[A-Z]{2}$", message = "Ländercode muss 2 Großbuchstaben sein") // @Pattern(regexp = "^[A-Z]{2}$", message = "Ländercode muss 2 Großbuchstaben sein")
String countryCode) { // String countryCode) {
//
log.info("Tariff rate request - HS Code: {}, Country: {}", hsCode, countryCode); // log.info("Tariff rate request - HS Code: {}, Country: {}", hsCode, countryCode);
//
try { // try {
TariffResult result = tariffService.getTariffRate(hsCode, countryCode); // TariffResult result = tariffService.getTariffRate(hsCode, countryCode);
//
if (result.isFound()) { // if (result.isFound()) {
TariffResponse response = TariffResponse.success( // TariffResponse response = TariffResponse.success(
result.getRate(), // result.getRate(),
result.getHsCode(), // result.getHsCode(),
result.getCountryCode() // result.getCountryCode()
); // );
return ResponseEntity.ok(response); // return ResponseEntity.ok(response);
} else { // } else {
TariffResponse response = TariffResponse.notFound(result.getMessage()); // TariffResponse response = TariffResponse.notFound(result.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response); // return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
} // }
//
} catch (Exception e) { // } catch (Exception e) {
log.error("Error getting tariff rate", e); // log.error("Error getting tariff rate", e);
TariffResponse response = TariffResponse.error("Internal server error: " + e.getMessage()); // TariffResponse response = TariffResponse.error("Internal server error: " + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); // return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
} // }
} // }
//
@GetMapping("/health") // @GetMapping("/health")
@Operation(summary = "Health Check", description = "Prüft ob der Service verfügbar ist") // @Operation(summary = "Health Check", description = "Prüft ob der Service verfügbar ist")
public ResponseEntity<Map<String, String>> health() { // public ResponseEntity<Map<String, String>> health() {
Map<String, String> response = new HashMap<>(); // Map<String, String> response = new HashMap<>();
response.put("status", "UP"); // response.put("status", "UP");
response.put("service", "TARIC Tariff Service"); // response.put("service", "TARIC Tariff Service");
return ResponseEntity.ok(response); // return ResponseEntity.ok(response);
} // }
//
/** // /**
* Response-Klasse für Tarif-Abfragen // * Response-Klasse für Tarif-Abfragen
*/ // */
public static class TariffResponse { // public static class TariffResponse {
private final boolean success; // private final boolean success;
private final BigDecimal tariffRate; // private final BigDecimal tariffRate;
private final String hsCode; // private final String hsCode;
private final String countryCode; // private final String countryCode;
private final String message; // private final String message;
private final String formattedRate; // private final String formattedRate;
//
private TariffResponse(boolean success, BigDecimal tariffRate, // private TariffResponse(boolean success, BigDecimal tariffRate,
String hsCode, String countryCode, String message) { // String hsCode, String countryCode, String message) {
this.success = success; // this.success = success;
this.tariffRate = tariffRate; // this.tariffRate = tariffRate;
this.hsCode = hsCode; // this.hsCode = hsCode;
this.countryCode = countryCode; // this.countryCode = countryCode;
this.message = message; // this.message = message;
this.formattedRate = tariffRate != null ? // this.formattedRate = tariffRate != null ?
tariffRate.toString() + " %" : null; // tariffRate.toString() + " %" : null;
} // }
//
public static TariffResponse success(BigDecimal rate, String hsCode, String countryCode) { // public static TariffResponse success(BigDecimal rate, String hsCode, String countryCode) {
return new TariffResponse(true, rate, hsCode, countryCode, // return new TariffResponse(true, rate, hsCode, countryCode,
"Tariff rate found successfully"); // "Tariff rate found successfully");
} // }
//
public static TariffResponse notFound(String message) { // public static TariffResponse notFound(String message) {
return new TariffResponse(false, null, null, null, message); // return new TariffResponse(false, null, null, null, message);
} // }
//
public static TariffResponse error(String message) { // public static TariffResponse error(String message) {
return new TariffResponse(false, null, null, null, message); // return new TariffResponse(false, null, null, null, message);
} // }
//
// Getters // // Getters
public boolean isSuccess() { return success; } // public boolean isSuccess() { return success; }
public BigDecimal getTariffRate() { return tariffRate; } // public BigDecimal getTariffRate() { return tariffRate; }
public String getHsCode() { return hsCode; } // public String getHsCode() { return hsCode; }
public String getCountryCode() { return countryCode; } // public String getCountryCode() { return countryCode; }
public String getMessage() { return message; } // public String getMessage() { return message; }
public String getFormattedRate() { return formattedRate; } // public String getFormattedRate() { return formattedRate; }
} // }
} }

View file

@ -3,20 +3,28 @@ package de.avatic.taric.model;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.Set; import java.util.Set;
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.jdbc.core.mapping.AggregateReference; import org.springframework.data.jdbc.core.mapping.AggregateReference;
import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.MappedCollection; import org.springframework.data.relational.core.mapping.MappedCollection;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import org.springframework.data.relational.core.mapping.Table;
@Getter
@Setter
@Table("applied_measure")
public class AppliedMeasure { public class AppliedMeasure {
@Id @Id
private Integer id; private Integer id;
@Column("start_date")
private LocalDate startDate; private LocalDate startDate;
@Column("end_date")
private LocalDate endDate; private LocalDate endDate;
private String amount; private String amount;
@ -44,84 +52,7 @@ public class AppliedMeasure {
@MappedCollection(idColumn = "applied_measure_id") @MappedCollection(idColumn = "applied_measure_id")
private Set<AppliedMeasureCondition> conditions; private Set<AppliedMeasureCondition> conditions;
public AppliedMeasure(AggregateReference<Measure, Integer> measure, Set<MeasureFootnote> measureFootnotes,
AggregateReference<LegalBase, Integer> legalBase, String additionalLegalBase, Set<MeasureExclusion> exclusions,
Set<AppliedMeasureCondition> 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<Measure, Integer> getMeasure() {
return measure;
}
public void setMeasure(final AggregateReference<Measure, Integer> measure) {
this.measure = measure;
}
public AggregateReference<LegalBase, Integer> getLegalBase() {
return legalBase;
}
public void setLegalBase(final AggregateReference<LegalBase, Integer> legalBase) {
this.legalBase = legalBase;
}
public Integer getOrderNumber() {
return orderNumber;
}
public Set<MeasureFootnote> getMeasureFootnotes() {
return measureFootnotes;
}
public Set<MeasureExclusion> getExclusions() {
return exclusions;
}
public Set<AppliedMeasureCondition> getConditions() {
return conditions;
}
} }

View file

@ -1,12 +1,17 @@
package de.avatic.taric.model; 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.annotation.Id;
import org.springframework.data.jdbc.core.mapping.AggregateReference; import org.springframework.data.jdbc.core.mapping.AggregateReference;
import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import jakarta.validation.constraints.NotNull; @Getter
@Setter
@Table("applied_measure_condition")
public class AppliedMeasureCondition { public class AppliedMeasureCondition {
@Id @Id
@ -37,82 +42,6 @@ public class AppliedMeasureCondition {
@Column("condition_type_id") @Column("condition_type_id")
private AggregateReference<ConditionType, Integer> conditionType; private AggregateReference<ConditionType, Integer> conditionType;
public AppliedMeasureCondition(AggregateReference<MeasureAction, Integer> measureAction,
AggregateReference<MonetaryUnit, Integer> monetaryUnit,
AggregateReference<Unit, Integer> unit,
AggregateReference<Certificate, Integer> certificate,
AggregateReference<ConditionType, Integer> 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<MeasureAction, Integer> getMeasureAction() {
return measureAction;
}
public void setMeasureAction(final AggregateReference<MeasureAction, Integer> measureAction) {
this.measureAction = measureAction;
}
public AggregateReference<MonetaryUnit, Integer> getMonetaryUnit() {
return monetaryUnit;
}
public void setMonetaryUnit(final AggregateReference<MonetaryUnit, Integer> monetaryUnit) {
this.monetaryUnit = monetaryUnit;
}
public AggregateReference<Unit, Integer> getUnit() {
return unit;
}
public void setUnit(final AggregateReference<Unit, Integer> unit) {
this.unit = unit;
}
public AggregateReference<Certificate, Integer> getCertificate() {
return certificate;
}
public void setCertificate(final AggregateReference<Certificate, Integer> certificate) {
this.certificate = certificate;
}
public AggregateReference<ConditionType, Integer> getConditionType() {
return conditionType;
}
public void setConditionType(final AggregateReference<ConditionType, Integer> conditionType) {
this.conditionType = conditionType;
}
} }

View file

@ -2,6 +2,8 @@ package de.avatic.taric.model;
import java.util.Set; import java.util.Set;
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.jdbc.core.mapping.AggregateReference; import org.springframework.data.jdbc.core.mapping.AggregateReference;
import org.springframework.data.relational.core.mapping.Column; 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; import jakarta.validation.constraints.NotNull;
@Setter
@Getter
@Table("import") @Table("import")
public class Import { public class Import {
@ -18,10 +22,10 @@ public class Import {
@NotNull @NotNull
@Column("nomenclature_id") @Column("nomenclature_id")
private AggregateReference<Nomenclature, Integer> nomenclature; private Integer nomenclature;
@Column("additional_code_id") @Column("additional_code_id")
private AggregateReference<AdditionalCode, Integer> additionalCode; private Integer additionalCode;
@Column("geo_id") @Column("geo_id")
private AggregateReference<Geo, Integer> geo; private AggregateReference<Geo, Integer> geo;
@ -32,56 +36,5 @@ public class Import {
@MappedCollection(idColumn = "import_id") @MappedCollection(idColumn = "import_id")
Set<AppliedMeasure> appliedMeasures; Set<AppliedMeasure> appliedMeasures;
public Import(AggregateReference<Nomenclature, Integer> nomenclature,
AggregateReference<AdditionalCode, Integer> additionalCode, Set<AppliedMeasure> appliedMeasures, AggregateReference<Geo,Integer> geo,
AggregateReference<GeoGroup, Integer> 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<Nomenclature, Integer> getNomenclature() {
return nomenclature;
}
public void setNomenclature(final AggregateReference<Nomenclature, Integer> nomenclature) {
this.nomenclature = nomenclature;
}
public AggregateReference<AdditionalCode, Integer> getAdditionalCode() {
return additionalCode;
}
public void setAdditionalCode(final AggregateReference<AdditionalCode, Integer> additionalCode) {
this.additionalCode = additionalCode;
}
public AggregateReference<Geo, Integer> getGeo() {
return geo;
}
public void setGeo(final AggregateReference<Geo, Integer> geo) {
this.geo = geo;
}
public AggregateReference<GeoGroup, Integer> getGeoGroup() {
return geoGroup;
}
public void setGeoGroup(final AggregateReference<GeoGroup, Integer> geoGroup) {
this.geoGroup = geoGroup;
}
} }

View file

@ -28,6 +28,7 @@ public class Measure {
private Integer tmCode; private Integer tmCode;
@Column("start_date")
private LocalDate startDate; private LocalDate startDate;
@MappedCollection(idColumn = "ref_id") @MappedCollection(idColumn = "ref_id")

View file

@ -7,20 +7,24 @@ import org.springframework.data.jdbc.core.mapping.AggregateReference;
import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.Column;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import org.springframework.data.relational.core.mapping.Table;
import java.time.LocalDate; import java.time.LocalDate;
@Getter @Getter
@Setter @Setter
@Table("measure_exclusion")
public class MeasureExclusion { public class MeasureExclusion {
@Id @Id
private Integer id; private Integer id;
LocalDate startDate; @Column("start_date")
private LocalDate startDate;
LocalDate endDate; @Column("end_date")
private LocalDate endDate;
@NotNull @NotNull
@Column("geo_id") @Column("geo_id")

View file

@ -6,21 +6,26 @@ import lombok.Setter;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.jdbc.core.mapping.AggregateReference; import org.springframework.data.jdbc.core.mapping.AggregateReference;
import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;
import java.time.LocalDate; import java.time.LocalDate;
@Getter @Getter
@Setter @Setter
@Table("measure_footnote")
public class MeasureFootnote { public class MeasureFootnote {
@Id @Id
private Integer id; private Integer id;
LocalDate startDate; @Column("start_date")
LocalDate endDate; private LocalDate startDate;
@Column("end_date")
private LocalDate endDate;
@Column("footnote_id") @Column("footnote_id")
private AggregateReference<Footnote, Integer> footnote; private Integer footnote;

View file

@ -9,6 +9,7 @@ import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.MappedCollection; import org.springframework.data.relational.core.mapping.MappedCollection;
import org.springframework.data.relational.core.mapping.Table;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.Set; import java.util.Set;
@ -16,6 +17,7 @@ import java.util.Set;
@Getter @Getter
@Setter @Setter
@Table("nomenclature")
public class Nomenclature { public class Nomenclature {
@Id @Id

View file

@ -1,7 +1,6 @@
package de.avatic.taric.repository; package de.avatic.taric.repository;
import de.avatic.taric.model.Import; import de.avatic.taric.model.Import;
import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.util.List; import java.util.List;
@ -9,9 +8,8 @@ import java.util.List;
@Repository @Repository
public interface ImportRepository extends CrudRepository<Import, Integer> { public interface ImportRepository extends CrudRepository<Import, Integer> {
@Query("SELECT * FROM import WHERE nomenclature_id = :nomenclatureId AND geo_id = :geoId") List<Import> findByNomenclatureAndGeo(Integer nomenclatureId, Integer geoId);
List<Import> findByNomenclatureIdAndGeoId(Integer nomenclatureId, Integer geoId);
List<Import> findByNomenclatureAndGeoGroupIn(Integer nomenclatureId, List<Integer> geoGroupIds);
@Query("SELECT * FROM import WHERE nomenclature_id = :nomenclatureId AND geo_group_id = :geoGroupId")
List<Import> findByNomenclatureIdAndGeoGroupId(Integer nomenclatureId, Integer geoGroupId);
} }

View file

@ -4,10 +4,7 @@ import de.avatic.taric.model.Nomenclature;
import de.avatic.taric.repository.NomenclatureRepository; import de.avatic.taric.repository.NomenclatureRepository;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.ArrayList; import java.util.*;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@Service @Service
public class NomenclatureService { public class NomenclatureService {
@ -19,7 +16,6 @@ public class NomenclatureService {
} }
public Optional<Nomenclature> getNomenclature(String hscode) { public Optional<Nomenclature> getNomenclature(String hscode) {
return nomenclatureRepository.findByHscode(normalize(hscode.replaceAll("[^0-9]", ""))); return nomenclatureRepository.findByHscode(normalize(hscode.replaceAll("[^0-9]", "")));
} }
@ -40,17 +36,22 @@ public class NomenclatureService {
if (hscode == null) return Collections.emptyList(); if (hscode == null) return Collections.emptyList();
List<String> cascade = getCascade(normalize(hscode.replaceAll("[^0-9]", ""))); List<String> 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(); return cascade.stream().map(nomenclatureRepository::findByHscode).flatMap(Optional::stream).toList();
} }
private List<String> getCascade(String hscode) { private List<String> getCascade(String hscode) {
var set = new HashSet<String>();
var parents = new ArrayList<String>(); var parents = new ArrayList<String>();
var hierarchyLevel = getHierarchyLevel(hscode); var hierarchyLevel = getHierarchyLevel(hscode);
while (hierarchyLevel > 0) { 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; hierarchyLevel -= 2;
} }

View file

@ -2,21 +2,32 @@ package de.avatic.taric.service;
import de.avatic.taric.error.ArgumentException; 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.model.Nomenclature;
import de.avatic.taric.repository.ImportRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
@Service @Service
@Slf4j
public class TariffService { public class TariffService {
private final GeoService geoService; private final GeoService geoService;
private final NomenclatureService nomenclatureService; 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.geoService = geoService;
this.nomenclatureService = nomenclatureService; this.nomenclatureService = nomenclatureService;
this.importRepository = importRepository;
} }
public void importTariffs(String hsCode, String countryCode) { public Optional<Double> importTariffs(String hsCode, String countryCode) {
var geoGroups = geoService.getGeoGroupByCountryCode(countryCode); var geoGroups = geoService.getGeoGroupByCountryCode(countryCode);
var geo = geoService.getGeo(countryCode); var geo = geoService.getGeo(countryCode);
@ -28,8 +39,80 @@ public class TariffService {
if (geo.isEmpty()) throw new ArgumentException("countryCode"); 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<Double> findTariff(Collection<AppliedMeasure> measures) {
List<Double> 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<AppliedMeasure> findAppliedMeasureByGeo(Nomenclature nomenclature, Geo geo) {
var foundImport = importRepository.findByNomenclatureAndGeo(nomenclature.getId(), geo.getId());
var applMeasures = new ArrayList<AppliedMeasure>();
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<AppliedMeasure> findAppliedMeasureByGeoGroup(Nomenclature nomenclature, Geo geo, List<GeoGroup> geoGroups) {
var foundImport = importRepository.findByNomenclatureAndGeoGroupIn(nomenclature.getId(), geoGroups.stream().map(GeoGroup::getId).toList());
var applMeasures = new ArrayList<AppliedMeasure>();
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;
}
}

View file

@ -37,267 +37,267 @@ public class TariffService2 {
this.measureExclusionRepository = measureExclusionRepository; this.measureExclusionRepository = measureExclusionRepository;
} }
/** // /**
* Ermittelt den Zolltarif für einen HS-Code und ein Herkunftsland // * Ermittelt den Zolltarif für einen HS-Code und ein Herkunftsland
* // *
* @param hsCode HS-Code (kann 6, 8 oder 10 Stellen haben) // * @param hsCode HS-Code (kann 6, 8 oder 10 Stellen haben)
* @param countryCode ISO-2 Ländercode (z.B. "CN" für China) // * @param countryCode ISO-2 Ländercode (z.B. "CN" für China)
* @return Zolltarif in Prozent // * @return Zolltarif in Prozent
*/ // */
@Transactional(readOnly = true) // @Transactional(readOnly = true)
public TariffResult getTariffRate(String hsCode, String countryCode) { // public TariffResult getTariffRate(String hsCode, String countryCode) {
log.info("Getting tariff rate for HS Code: {} from country: {}", hsCode, countryCode); // log.info("Getting tariff rate for HS Code: {} from country: {}", hsCode, countryCode);
//
// Normalisiere HS-Code auf 10 Stellen // // Normalisiere HS-Code auf 10 Stellen
String normalizedHsCode = normalizeHsCode(hsCode); // String normalizedHsCode = normalizeHsCode(hsCode);
//
// Finde alle relevanten Nomenclature-Codes (inkl. Parent-Codes durch Cascade-Prinzip) // // Finde alle relevanten Nomenclature-Codes (inkl. Parent-Codes durch Cascade-Prinzip)
List<String> relevantCodes = findRelevantNomenclatureCodes(normalizedHsCode); // List<String> relevantCodes = findRelevantNomenclatureCodes(normalizedHsCode);
log.debug("Found relevant codes: {}", relevantCodes); // log.debug("Found relevant codes: {}", relevantCodes);
//
// Hole das Land // // Hole das Land
Optional<Geo> countryOpt = geoRepository.findByIso3166Code(countryCode); // Optional<Geo> countryOpt = geoRepository.findByIso3166Code(countryCode);
if (countryOpt.isEmpty()) { // if (countryOpt.isEmpty()) {
log.warn("Country not found: {}", countryCode); // log.warn("Country not found: {}", countryCode);
return TariffResult.notFound("Country code not found: " + countryCode); // return TariffResult.notFound("Country code not found: " + countryCode);
} // }
//
Geo country = countryOpt.get(); // Geo country = countryOpt.get();
//
// Finde alle Imports für die relevanten Codes // // Finde alle Imports für die relevanten Codes
List<Import> imports = findRelevantImports(relevantCodes, country); // List<Import> imports = findRelevantImports(relevantCodes, country);
//
if (imports.isEmpty()) { // if (imports.isEmpty()) {
log.info("No imports found for codes: {} and country: {}", relevantCodes, countryCode); // log.info("No imports found for codes: {} and country: {}", relevantCodes, countryCode);
// Versuche Erga Omnes (alle Länder) // // Versuche Erga Omnes (alle Länder)
imports = findErgaOmnesImports(relevantCodes); // imports = findErgaOmnesImports(relevantCodes);
} // }
//
if (imports.isEmpty()) { // if (imports.isEmpty()) {
return TariffResult.notFound("No tariff data found for HS code: " + hsCode); // return TariffResult.notFound("No tariff data found for HS code: " + hsCode);
} // }
//
// Finde die anwendbaren Maßnahmen // // Finde die anwendbaren Maßnahmen
BigDecimal tariffRate = calculateTariffRate(imports, country); // BigDecimal tariffRate = calculateTariffRate(imports, country);
//
return TariffResult.success(tariffRate, normalizedHsCode, countryCode); // return TariffResult.success(tariffRate, normalizedHsCode, countryCode);
} // }
//
private String normalizeHsCode(String hsCode) { // private String normalizeHsCode(String hsCode) {
// Entferne alle nicht-numerischen Zeichen // // Entferne alle nicht-numerischen Zeichen
String cleaned = hsCode.replaceAll("[^0-9]", ""); // String cleaned = hsCode.replaceAll("[^0-9]", "");
//
// Fülle auf 10 Stellen mit Nullen auf // // Fülle auf 10 Stellen mit Nullen auf
while (cleaned.length() < 10) { // while (cleaned.length() < 10) {
cleaned += "0"; // cleaned += "0";
} // }
//
// Begrenze auf 10 Stellen // // Begrenze auf 10 Stellen
if (cleaned.length() > 10) { // if (cleaned.length() > 10) {
cleaned = cleaned.substring(0, 10); // cleaned = cleaned.substring(0, 10);
} // }
//
return cleaned; // return cleaned;
} // }
//
private List<String> findRelevantNomenclatureCodes(String hsCode) { // private List<String> findRelevantNomenclatureCodes(String hsCode) {
List<String> codes = new ArrayList<>(); // List<String> codes = new ArrayList<>();
codes.add(hsCode); // codes.add(hsCode);
//
// Füge Parent-Codes hinzu (Cascade-Prinzip) // // Füge Parent-Codes hinzu (Cascade-Prinzip)
// Beispiel: 8504101010 -> auch 85041010, 850410, 8504, 85 // // Beispiel: 8504101010 -> auch 85041010, 850410, 8504, 85
String code = hsCode; // String code = hsCode;
while (code.length() > 2) { // while (code.length() > 2) {
// Entferne die letzten 2 Nullen // // Entferne die letzten 2 Nullen
if (code.endsWith("00")) { // if (code.endsWith("00")) {
code = code.substring(0, code.length() - 2); // code = code.substring(0, code.length() - 2);
codes.add(code + "0".repeat(10 - code.length())); // codes.add(code + "0".repeat(10 - code.length()));
} else { // } else {
break; // break;
} // }
} // }
//
return codes; // return codes;
} // }
//
private List<Import> findRelevantImports(List<String> nomenclatureCodes, Geo country) { // private List<Import> findRelevantImports(List<String> nomenclatureCodes, Geo country) {
List<Import> imports = new ArrayList<>(); // List<Import> imports = new ArrayList<>();
//
for (String code : nomenclatureCodes) { // for (String code : nomenclatureCodes) {
// Suche direkte Länder-Zuordnungen // // Suche direkte Länder-Zuordnungen
Optional<Nomenclature> nomenclatureOpt = nomenclatureRepository.findByHscode(code); // Optional<Nomenclature> nomenclatureOpt = nomenclatureRepository.findByHscode(code);
if (nomenclatureOpt.isPresent()) { // if (nomenclatureOpt.isPresent()) {
Nomenclature nomenclature = nomenclatureOpt.get(); // Nomenclature nomenclature = nomenclatureOpt.get();
//
// Suche Imports mit direkter Geo-Zuordnung // // Suche Imports mit direkter Geo-Zuordnung
imports.addAll(importRepository.findByNomenclatureIdAndGeoId( // imports.addAll(importRepository.findByNomenclatureIdAndGeoId(
nomenclature.getId(), country.getId())); // nomenclature.getId(), country.getId()));
//
// Suche auch nach Ländergruppen-Mitgliedschaften // // Suche auch nach Ländergruppen-Mitgliedschaften
List<GeoGroupMembership> memberships = // List<GeoGroupMembership> memberships =
geoGroupMembershipRepository.findByGeoId(country.getId()); // geoGroupMembershipRepository.findByGeoId(country.getId());
//
for (GeoGroupMembership membership : memberships) { // for (GeoGroupMembership membership : memberships) {
// Hole die geo_group_id aus der membership // // Hole die geo_group_id aus der membership
Integer geoGroupId = geoGroupMembershipRepository // Integer geoGroupId = geoGroupMembershipRepository
.findGeoGroupIdByMembershipId(membership.getId()); // .findGeoGroupIdByMembershipId(membership.getId());
if (geoGroupId != null) { // if (geoGroupId != null) {
imports.addAll(importRepository.findByNomenclatureIdAndGeoGroupId( // imports.addAll(importRepository.findByNomenclatureIdAndGeoGroupId(
nomenclature.getId(), geoGroupId)); // nomenclature.getId(), geoGroupId));
} // }
} // }
} // }
} // }
//
return imports; // return imports;
} // }
//
private List<Import> findErgaOmnesImports(List<String> nomenclatureCodes) { // private List<Import> findErgaOmnesImports(List<String> nomenclatureCodes) {
List<Import> imports = new ArrayList<>(); // List<Import> imports = new ArrayList<>();
//
// Erga Omnes hat normalerweise den Code "1011" // // Erga Omnes hat normalerweise den Code "1011"
Optional<GeoGroup> ergaOmnes = geoGroupRepository.findByGeoGroupCode("1011"); // Optional<GeoGroup> ergaOmnes = geoGroupRepository.findByGeoGroupCode("1011");
if (ergaOmnes.isEmpty()) { // if (ergaOmnes.isEmpty()) {
return imports; // return imports;
} // }
//
for (String code : nomenclatureCodes) { // for (String code : nomenclatureCodes) {
Optional<Nomenclature> nomenclatureOpt = nomenclatureRepository.findByHscode(code); // Optional<Nomenclature> nomenclatureOpt = nomenclatureRepository.findByHscode(code);
if (nomenclatureOpt.isPresent()) { // if (nomenclatureOpt.isPresent()) {
imports.addAll(importRepository.findByNomenclatureIdAndGeoGroupId( // imports.addAll(importRepository.findByNomenclatureIdAndGeoGroupId(
nomenclatureOpt.get().getId(), ergaOmnes.get().getId())); // nomenclatureOpt.get().getId(), ergaOmnes.get().getId()));
} // }
} // }
//
return imports; // return imports;
} // }
//
private BigDecimal calculateTariffRate(List<Import> imports, Geo country) { // private BigDecimal calculateTariffRate(List<Import> imports, Geo country) {
BigDecimal lowestRate = null; // BigDecimal lowestRate = null;
LocalDate today = LocalDate.now(); // LocalDate today = LocalDate.now();
//
for (Import imp : imports) { // for (Import imp : imports) {
// Nutze die korrekte Repository-Methode mit Import-Reference // // Nutze die korrekte Repository-Methode mit Import-Reference
List<AppliedMeasure> measures = appliedMeasureRepository // List<AppliedMeasure> measures = appliedMeasureRepository
.findByImport(AggregateReference.to(imp.getId())); // .findByImport(AggregateReference.to(imp.getId()));
//
for (AppliedMeasure appliedMeasure : measures) { // for (AppliedMeasure appliedMeasure : measures) {
// Prüfe ob Maßnahme gültig ist // // Prüfe ob Maßnahme gültig ist
if (!isMeasureValid(appliedMeasure, today)) { // if (!isMeasureValid(appliedMeasure, today)) {
continue; // continue;
} // }
//
// Prüfe ob das Land ausgeschlossen ist // // Prüfe ob das Land ausgeschlossen ist
if (isCountryExcluded(appliedMeasure, country)) { // if (isCountryExcluded(appliedMeasure, country)) {
continue; // continue;
} // }
//
// Hole die Maßnahme über die AggregateReference // // Hole die Maßnahme über die AggregateReference
if (appliedMeasure.getMeasure() == null) { // if (appliedMeasure.getMeasure() == null) {
continue; // continue;
} // }
//
Optional<Measure> measureOpt = measureRepository.findById( // Optional<Measure> measureOpt = measureRepository.findById(
appliedMeasure.getMeasure().getId()); // appliedMeasure.getMeasure().getId());
if (measureOpt.isEmpty()) { // if (measureOpt.isEmpty()) {
continue; // continue;
} // }
//
Measure measure = measureOpt.get(); // Measure measure = measureOpt.get();
//
// Wir interessieren uns hauptsächlich für Third Country Duty (103) // // Wir interessieren uns hauptsächlich für Third Country Duty (103)
// und Preferential Tariff (142, 143) // // und Preferential Tariff (142, 143)
if (!"103".equals(measure.getMeasureCode()) && // if (!"103".equals(measure.getMeasureCode()) &&
!"142".equals(measure.getMeasureCode()) && // !"142".equals(measure.getMeasureCode()) &&
!"143".equals(measure.getMeasureCode())) { // !"143".equals(measure.getMeasureCode())) {
continue; // continue;
} // }
//
// Parse den Zollsatz aus dem amount String // // Parse den Zollsatz aus dem amount String
BigDecimal rate = parseTariffRate(appliedMeasure.getAmount()); // BigDecimal rate = parseTariffRate(appliedMeasure.getAmount());
if (rate != null && (lowestRate == null || rate.compareTo(lowestRate) < 0)) { // if (rate != null && (lowestRate == null || rate.compareTo(lowestRate) < 0)) {
lowestRate = rate; // lowestRate = rate;
} // }
} // }
} // }
//
return lowestRate != null ? lowestRate : BigDecimal.ZERO; // return lowestRate != null ? lowestRate : BigDecimal.ZERO;
} // }
//
private boolean isMeasureValid(AppliedMeasure measure, LocalDate date) { // private boolean isMeasureValid(AppliedMeasure measure, LocalDate date) {
if (measure.getStartDate() != null && measure.getStartDate().isAfter(date)) { // if (measure.getStartDate() != null && measure.getStartDate().isAfter(date)) {
return false; // return false;
} // }
if (measure.getEndDate() != null && measure.getEndDate().isBefore(date)) { // if (measure.getEndDate() != null && measure.getEndDate().isBefore(date)) {
return false; // return false;
} // }
return true; // return true;
} // }
//
private boolean isCountryExcluded(AppliedMeasure measure, Geo country) { // private boolean isCountryExcluded(AppliedMeasure measure, Geo country) {
// Finde Exclusions für diese AppliedMeasure // // Finde Exclusions für diese AppliedMeasure
List<MeasureExclusion> exclusions = // List<MeasureExclusion> exclusions =
measureExclusionRepository.findByAppliedMeasure( // measureExclusionRepository.findByAppliedMeasure(
AggregateReference.to(measure.getId())); // AggregateReference.to(measure.getId()));
//
return exclusions.stream() // return exclusions.stream()
.anyMatch(exc -> exc.getGeo() != null && // .anyMatch(exc -> exc.getGeo() != null &&
exc.getGeo().getId().equals(country.getId())); // exc.getGeo().getId().equals(country.getId()));
} // }
//
private BigDecimal parseTariffRate(String amount) { // private BigDecimal parseTariffRate(String amount) {
if (amount == null || amount.isEmpty()) { // if (amount == null || amount.isEmpty()) {
return null; // return null;
} // }
//
// Einfacher Parser für Prozentsätze // // Einfacher Parser für Prozentsätze
// Format: "12.5 %" oder "12.5% + ..." oder "12.5 % MAX ..." // // Format: "12.5 %" oder "12.5% + ..." oder "12.5 % MAX ..."
Pattern pattern = Pattern.compile("^([0-9]+\\.?[0-9]*)\\s*%"); // Pattern pattern = Pattern.compile("^([0-9]+\\.?[0-9]*)\\s*%");
Matcher matcher = pattern.matcher(amount); // Matcher matcher = pattern.matcher(amount);
//
if (matcher.find()) { // if (matcher.find()) {
try { // try {
return new BigDecimal(matcher.group(1)); // return new BigDecimal(matcher.group(1));
} catch (NumberFormatException e) { // } catch (NumberFormatException e) {
log.warn("Could not parse tariff rate from: {}", amount); // log.warn("Could not parse tariff rate from: {}", amount);
} // }
} // }
//
return null; // return null;
} // }
//
/** // /**
* Result-Klasse für Tarif-Abfragen // * Result-Klasse für Tarif-Abfragen
*/ // */
public static class TariffResult { // public static class TariffResult {
private final boolean found; // private final boolean found;
private final BigDecimal rate; // private final BigDecimal rate;
private final String hsCode; // private final String hsCode;
private final String countryCode; // private final String countryCode;
private final String message; // private final String message;
//
private TariffResult(boolean found, BigDecimal rate, String hsCode, // private TariffResult(boolean found, BigDecimal rate, String hsCode,
String countryCode, String message) { // String countryCode, String message) {
this.found = found; // this.found = found;
this.rate = rate; // this.rate = rate;
this.hsCode = hsCode; // this.hsCode = hsCode;
this.countryCode = countryCode; // this.countryCode = countryCode;
this.message = message; // this.message = message;
} // }
//
public static TariffResult success(BigDecimal rate, String hsCode, String countryCode) { // public static TariffResult success(BigDecimal rate, String hsCode, String countryCode) {
return new TariffResult(true, rate, hsCode, countryCode, null); // return new TariffResult(true, rate, hsCode, countryCode, null);
} // }
//
public static TariffResult notFound(String message) { // public static TariffResult notFound(String message) {
return new TariffResult(false, null, null, null, message); // return new TariffResult(false, null, null, null, message);
} // }
//
// Getters // // Getters
public boolean isFound() { return found; } // public boolean isFound() { return found; }
public BigDecimal getRate() { return rate; } // public BigDecimal getRate() { return rate; }
public String getHsCode() { return hsCode; } // public String getHsCode() { return hsCode; }
public String getCountryCode() { return countryCode; } // public String getCountryCode() { return countryCode; }
public String getMessage() { return message; } // public String getMessage() { return message; }
} // }
} }

View file

@ -1 +1,2 @@
spring.application.name=taric spring.application.name=taric
logging.level.org.springframework.data.jdbc.core.convert.RowDocumentResultSetExtractor=ERROR

View file

@ -73,7 +73,8 @@ create table if not exists `measure`
create table if not exists `measure_action` create table if not exists `measure_action`
( (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, 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 -- geo
@ -82,7 +83,8 @@ create table if not exists `geo`
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
iso_3166_code CHAR(2), iso_3166_code CHAR(2),
start_date DATE, start_date DATE,
end_date DATE end_date DATE,
CONSTRAINT UC_IsoCode UNIQUE (iso_3166_code)
); );
create table if not exists `geo_group` create table if not exists `geo_group`
@ -91,7 +93,8 @@ create table if not exists `geo_group`
geo_group_code CHAR(4), geo_group_code CHAR(4),
abbr TEXT, abbr TEXT,
start_date DATE, start_date DATE,
end_date DATE end_date DATE,
CONSTRAINT UC_GroupCode UNIQUE (geo_group_code)
); );
create table if not exists `geo_group_membership` create table if not exists `geo_group_membership`
@ -102,7 +105,8 @@ create table if not exists `geo_group_membership`
start_date DATE, start_date DATE,
end_date DATE, end_date DATE,
FOREIGN KEY (geo_id) REFERENCES geo(id), 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 -- nomenclature
@ -134,7 +138,8 @@ create table if not exists `additional_code`
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
additional_code CHAR(4), additional_code CHAR(4),
start_date DATE, start_date DATE,
end_date DATE end_date DATE,
CONSTRAINT UC_AdditionalCode UNIQUE (additional_code)
); );
-- import, applied_measures -- import, applied_measures