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/>
</scm>
<properties>
<java.version>17</java.version>
<java.version>23</java.version>
</properties>
<dependencies>
<dependency>

View file

@ -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<Double> getTariffRate(@RequestParam String hsCode, @RequestParam String countryCode) {
return tariffService.importTariffs( hsCode, countryCode).map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build());
}
}

View file

@ -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<TariffResponse> 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<Map<String, String>> health() {
Map<String, String> 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<TariffResponse> 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<Map<String, String>> health() {
// Map<String, String> 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; }
// }
}

View file

@ -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<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;
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, 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 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,10 +22,10 @@ public class Import {
@NotNull
@Column("nomenclature_id")
private AggregateReference<Nomenclature, Integer> nomenclature;
private Integer nomenclature;
@Column("additional_code_id")
private AggregateReference<AdditionalCode, Integer> additionalCode;
private Integer additionalCode;
@Column("geo_id")
private AggregateReference<Geo, Integer> geo;
@ -32,56 +36,5 @@ public class Import {
@MappedCollection(idColumn = "import_id")
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;
@Column("start_date")
private LocalDate startDate;
@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 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")

View file

@ -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, Integer> footnote;
private Integer footnote;

View file

@ -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

View file

@ -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<Import, Integer> {
@Query("SELECT * FROM import WHERE nomenclature_id = :nomenclatureId AND geo_id = :geoId")
List<Import> findByNomenclatureIdAndGeoId(Integer nomenclatureId, Integer geoId);
List<Import> findByNomenclatureAndGeo(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 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<Nomenclature> 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<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();
}
private List<String> getCascade(String hscode) {
var set = new HashSet<String>();
var parents = new ArrayList<String>();
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) {

View file

@ -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<Double> 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<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;
}
/**
* 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<String> relevantCodes = findRelevantNomenclatureCodes(normalizedHsCode);
log.debug("Found relevant codes: {}", relevantCodes);
// Hole das Land
Optional<Geo> 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<Import> 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<String> findRelevantNomenclatureCodes(String hsCode) {
List<String> 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<Import> findRelevantImports(List<String> nomenclatureCodes, Geo country) {
List<Import> imports = new ArrayList<>();
for (String code : nomenclatureCodes) {
// Suche direkte Länder-Zuordnungen
Optional<Nomenclature> 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<GeoGroupMembership> 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<Import> findErgaOmnesImports(List<String> nomenclatureCodes) {
List<Import> imports = new ArrayList<>();
// Erga Omnes hat normalerweise den Code "1011"
Optional<GeoGroup> ergaOmnes = geoGroupRepository.findByGeoGroupCode("1011");
if (ergaOmnes.isEmpty()) {
return imports;
}
for (String code : nomenclatureCodes) {
Optional<Nomenclature> nomenclatureOpt = nomenclatureRepository.findByHscode(code);
if (nomenclatureOpt.isPresent()) {
imports.addAll(importRepository.findByNomenclatureIdAndGeoGroupId(
nomenclatureOpt.get().getId(), ergaOmnes.get().getId()));
}
}
return imports;
}
private BigDecimal calculateTariffRate(List<Import> imports, Geo country) {
BigDecimal lowestRate = null;
LocalDate today = LocalDate.now();
for (Import imp : imports) {
// Nutze die korrekte Repository-Methode mit Import-Reference
List<AppliedMeasure> 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<Measure> 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<MeasureExclusion> 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<String> relevantCodes = findRelevantNomenclatureCodes(normalizedHsCode);
// log.debug("Found relevant codes: {}", relevantCodes);
//
// // Hole das Land
// Optional<Geo> 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<Import> 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<String> findRelevantNomenclatureCodes(String hsCode) {
// List<String> 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<Import> findRelevantImports(List<String> nomenclatureCodes, Geo country) {
// List<Import> imports = new ArrayList<>();
//
// for (String code : nomenclatureCodes) {
// // Suche direkte Länder-Zuordnungen
// Optional<Nomenclature> 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<GeoGroupMembership> 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<Import> findErgaOmnesImports(List<String> nomenclatureCodes) {
// List<Import> imports = new ArrayList<>();
//
// // Erga Omnes hat normalerweise den Code "1011"
// Optional<GeoGroup> ergaOmnes = geoGroupRepository.findByGeoGroupCode("1011");
// if (ergaOmnes.isEmpty()) {
// return imports;
// }
//
// for (String code : nomenclatureCodes) {
// Optional<Nomenclature> nomenclatureOpt = nomenclatureRepository.findByHscode(code);
// if (nomenclatureOpt.isPresent()) {
// imports.addAll(importRepository.findByNomenclatureIdAndGeoGroupId(
// nomenclatureOpt.get().getId(), ergaOmnes.get().getId()));
// }
// }
//
// return imports;
// }
//
// private BigDecimal calculateTariffRate(List<Import> imports, Geo country) {
// BigDecimal lowestRate = null;
// LocalDate today = LocalDate.now();
//
// for (Import imp : imports) {
// // Nutze die korrekte Repository-Methode mit Import-Reference
// List<AppliedMeasure> 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<Measure> 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<MeasureExclusion> 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; }
// }
}

View file

@ -1 +1,2 @@
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`
(
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