Compare commits

..

12 commits
v1.0.8 ... main

Author SHA1 Message Date
85f660665a Merge pull request 'dev' (#108) from dev into main
Reviewed-on: #108
2026-01-22 16:52:42 +00:00
Jan
605bcfe0fc Improve Excel mapper geocoding: Add fuzzy search fallback for failed addresses, introduce better cell value handling, and enhance error logging. 2026-01-22 17:25:40 +01:00
03cd1274e9 src/main/java/de/avatic/lcc/service/api/BatchGeoApiService.java aktualisiert
Fixed nullpointer exception in batch geo coding
2026-01-22 09:35:12 +00:00
8e01ef055a Merge pull request 'Implemented deactivated container types, CalculationExecutionService to handle container type activation logic. Fix destinationEdit store to handle null values in data.' (#107) from dev into main
Reviewed-on: #107
2026-01-20 08:57:30 +00:00
Jan
462a960c68 Implemented deactivated container types, CalculationExecutionService to handle container type activation logic. Fix destinationEdit store to handle null values in data. 2026-01-20 09:53:58 +01:00
8be5f34137 Merge pull request 'dev' (#106) from dev into main
Reviewed-on: #106
2026-01-18 21:39:04 +00:00
Jan
b66ac66b54 Bugfixing: Clean up existing drafts in PremisesService and include annual repacking cost in CalculationExecutionService. 2026-01-18 21:24:24 +01:00
Jan
11d32a665e Rename pages-change event to page-change in Pagination component and update related references. 2026-01-18 18:37:07 +01:00
Jan
b5f2df8be7 Rename helppages-change event to pages-change in Pagination component. 2026-01-18 18:15:45 +01:00
Jan
c1e136f914 Merge remote-tracking branch 'origin/dev' into dev 2026-01-18 18:11:32 +01:00
Jan
bdfaef3365 Fixed isWeightExceeded value in database. Fixed shipping freqency rounding in database 2026-01-18 18:11:22 +01:00
ac23dc4728 Merge pull request 'main' (#105) from main into dev
Reviewed-on: #105
2026-01-11 22:27:40 +00:00
9 changed files with 408 additions and 27 deletions

View file

@ -10,7 +10,7 @@
<PhCaretLeft :size="18" /> Previous
</button>
<!-- First helppages -->
<!-- First pages -->
<button
v-if="showFirstPage"
class="pagination-btn page-number"
@ -23,7 +23,7 @@
<!-- First ellipsis -->
<span v-if="showFirstEllipsis" class="ellipsis">...</span>
<!-- Page numbers around current helppages -->
<!-- Page numbers around current pages -->
<button
v-for="pageNum in visiblePages"
:key="pageNum"
@ -37,7 +37,7 @@
<!-- Last ellipsis -->
<span v-if="showLastEllipsis" class="ellipsis">...</span>
<!-- Last helppages -->
<!-- Last pages -->
<button
v-if="showLastPage"
class="pagination-btn page-number"
@ -90,7 +90,7 @@ export default {
default: 5
}
},
emits: ['helppages-change'],
emits: ['page-change'],
computed: {
visiblePages() {
const delta = Math.floor(this.maxVisiblePages / 2);
@ -130,7 +130,7 @@ export default {
methods: {
goToPage(pageNumber) {
if (pageNumber >= 1 && pageNumber <= this.pageCount && pageNumber !== this.page) {
this.$emit('helppages-change', pageNumber);
this.$emit('page-change', pageNumber);
}
}
}

View file

@ -105,8 +105,9 @@ export const useDestinationEditStore = defineStore('destinationEdit', {
const toBeDeleted = toBeDeletedMap.get(premiseId);
const filtered = destinations !== null ? destinations.filter(d => !toBeDeleted?.includes(d.destination_node.id)) : [];
const dataForPremiseId = (data[premiseId] ?? null) === null ? [] : data[premiseId];
this.destinations.set(premiseId, [...filtered, ...data[premiseId]]);
this.destinations.set(premiseId, [...filtered, ...dataForPremiseId]);
});
} catch (error) {

View file

@ -0,0 +1,57 @@
package de.avatic.lcc.model.azuremaps.geocoding.fuzzy;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import java.util.List;
@JsonIgnoreProperties(ignoreUnknown = true)
public class FuzzySearchResponse {
private Summary summary;
private List<FuzzySearchResult> results;
public Summary getSummary() {
return summary;
}
public void setSummary(Summary summary) {
this.summary = summary;
}
public List<FuzzySearchResult> getResults() {
return results;
}
public void setResults(List<FuzzySearchResult> results) {
this.results = results;
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Summary {
private String query;
private int numResults;
private int totalResults;
public String getQuery() {
return query;
}
public void setQuery(String query) {
this.query = query;
}
public int getNumResults() {
return numResults;
}
public void setNumResults(int numResults) {
this.numResults = numResults;
}
public int getTotalResults() {
return totalResults;
}
public void setTotalResults(int totalResults) {
this.totalResults = totalResults;
}
}
}

View file

@ -0,0 +1,150 @@
package de.avatic.lcc.model.azuremaps.geocoding.fuzzy;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
public class FuzzySearchResult {
private String type;
private double score;
private Position position;
private Address address;
private String entityType;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public double getScore() {
return score;
}
public void setScore(double score) {
this.score = score;
}
public Position getPosition() {
return position;
}
public void setPosition(Position position) {
this.position = position;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
public String getEntityType() {
return entityType;
}
public void setEntityType(String entityType) {
this.entityType = entityType;
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Position {
private double lat;
private double lon;
public double getLat() {
return lat;
}
public void setLat(double lat) {
this.lat = lat;
}
public double getLon() {
return lon;
}
public void setLon(double lon) {
this.lon = lon;
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Address {
private String freeformAddress;
private String countryCode;
private String countryCodeISO3;
private String country;
private String municipality;
private String postalCode;
private String streetName;
private String streetNumber;
public String getFreeformAddress() {
return freeformAddress;
}
public void setFreeformAddress(String freeformAddress) {
this.freeformAddress = freeformAddress;
}
public String getCountryCode() {
return countryCode;
}
public void setCountryCode(String countryCode) {
this.countryCode = countryCode;
}
public String getCountryCodeISO3() {
return countryCodeISO3;
}
public void setCountryCodeISO3(String countryCodeISO3) {
this.countryCodeISO3 = countryCodeISO3;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
public String getMunicipality() {
return municipality;
}
public void setMunicipality(String municipality) {
this.municipality = municipality;
}
public String getPostalCode() {
return postalCode;
}
public void setPostalCode(String postalCode) {
this.postalCode = postalCode;
}
public String getStreetName() {
return streetName;
}
public void setStreetName(String streetName) {
this.streetName = streetName;
}
public String getStreetNumber() {
return streetNumber;
}
public void setStreetNumber(String streetNumber) {
this.streetNumber = streetNumber;
}
}
}

View file

@ -177,6 +177,12 @@ public class PremisesService {
premissIds.forEach(id -> {
var old = premiseRepository.getPremiseById(id).orElseThrow();
var existingPremises = premiseRepository.findByMaterialIdAndSupplierId(old.getMaterialId(), old.getSupplierNodeId(), old.getUserSupplierNodeId(), userId);
var existingDrafts = existingPremises.stream().filter(p -> p.getState().equals(PremiseState.DRAFT)).toList();
this.delete(existingDrafts.stream().map(Premise::getId).toList());
var newId = premiseRepository.insert(old.getMaterialId(), old.getSupplierNodeId(), old.getUserSupplierNodeId(), BigDecimal.valueOf(old.getLocation().getLatitude()), BigDecimal.valueOf(old.getLocation().getLongitude()), old.getCountryId(), userId);
premiseRepository.updateMaterial(Collections.singletonList(newId), old.getHsCode(), old.getTariffRate(), old.getTariffUnlocked());

View file

@ -123,10 +123,21 @@ public class PropertyService {
}
@SuppressWarnings("unchecked")
public <T> Optional<T> getProperty(SystemPropertyMappingId mappingId, Integer setId) {
var prop = propertyRepository.getPropertyByMappingId(mappingId, setId);
return doCasting(prop);
}
public <T> Optional<T> getProperty(SystemPropertyMappingId mappingId) {
var prop = propertyRepository.getPropertyByMappingId(mappingId);
return doCasting(prop);
}
@SuppressWarnings("unchecked")
private <T> Optional<T> doCasting(Optional<PropertyDTO> prop) {
if (prop.isEmpty())
return Optional.empty();
@ -158,4 +169,6 @@ public class PropertyService {
default -> throw new IllegalArgumentException("Unsupported data type: " + dataType);
};
}
}

View file

@ -1,11 +1,12 @@
package de.avatic.lcc.service.api;
import de.avatic.lcc.model.excel.ExcelNode;
import de.avatic.lcc.model.azuremaps.geocoding.batch.BatchGeocodingRequest;
import de.avatic.lcc.model.azuremaps.geocoding.batch.BatchGeocodingResponse;
import de.avatic.lcc.model.azuremaps.geocoding.batch.BatchItem;
import de.avatic.lcc.model.azuremaps.geocoding.fuzzy.FuzzySearchResponse;
import de.avatic.lcc.model.bulk.BulkInstruction;
import de.avatic.lcc.model.db.country.IsoCode;
import de.avatic.lcc.model.excel.ExcelNode;
import de.avatic.lcc.util.exception.internalerror.ExcelValidationError;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -17,6 +18,8 @@ import org.springframework.web.util.UriComponentsBuilder;
import java.math.BigDecimal;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@ -45,6 +48,7 @@ public class BatchGeoApiService {
}
ArrayList<BulkInstruction<ExcelNode>> noGeo = new ArrayList<>();
ArrayList<BulkInstruction<ExcelNode>> failedGeoLookups = new ArrayList<>();
int totalSuccessful = 0;
for (var node : nodes) {
@ -53,6 +57,7 @@ public class BatchGeoApiService {
}
}
for (int currentBatch = 0; currentBatch < noGeo.size(); currentBatch += MAX_BATCH_SIZE) {
int end = Math.min(currentBatch + MAX_BATCH_SIZE, noGeo.size());
var chunk = noGeo.subList(currentBatch, end);
@ -73,7 +78,8 @@ public class BatchGeoApiService {
if (!result.getFeatures().isEmpty() &&
(result.getFeatures().getFirst().getProperties().getConfidence().equalsIgnoreCase("high") ||
result.getFeatures().getFirst().getProperties().getConfidence().equalsIgnoreCase("medium") ||
result.getFeatures().getFirst().getProperties().getMatchCodes().stream().anyMatch(s -> s.equalsIgnoreCase("good")))) {
(result.getFeatures().getFirst().getProperties().getMatchCodes() != null &&
result.getFeatures().getFirst().getProperties().getMatchCodes().stream().anyMatch(s -> s.equalsIgnoreCase("good"))))) {
var geometry = result.getFeatures().getFirst().getGeometry();
var properties = result.getFeatures().getFirst().getProperties();
node.setGeoLng(BigDecimal.valueOf(geometry.getCoordinates().get(0)));
@ -82,11 +88,106 @@ public class BatchGeoApiService {
node.setCountryId(IsoCode.valueOf(properties.getAddress().getCountryRegion().getIso()));
} else {
logger.warn("Geocoding failed for address {}", node.getAddress());
throw new ExcelValidationError("Unable to geocode " + node.getName() + ". Please check your address or enter geo position yourself.");
failedGeoLookups.add(chunk.get(itemIdx));
//throw new ExcelValidationError("Unable to geocode " + node.getName() + ". Please check your address or enter geo position yourself.");
}
}
}
}
// Second pass: fuzzy lookup with company name for failed addresses
if (!failedGeoLookups.isEmpty()) {
logger.info("Retrying {} failed lookups with fuzzy search", failedGeoLookups.size());
int fuzzySuccessful = 0;
for (var instruction : failedGeoLookups) {
var node = instruction.getEntity();
var fuzzyResult = executeFuzzySearch(node);
if (fuzzyResult.isPresent() && fuzzyResult.get().getResults() != null
&& !fuzzyResult.get().getResults().isEmpty()) {
var result = fuzzyResult.get().getResults().getFirst();
// Score >= 0.7 means good confidence (1.0 = perfect match)
if (result.getScore() >= 7.0) {
node.setGeoLat(BigDecimal.valueOf(result.getPosition().getLat()));
node.setGeoLng(BigDecimal.valueOf(result.getPosition().getLon()));
node.setAddress(result.getAddress().getFreeformAddress());
// Update country if it differs
if (result.getAddress().getCountryCode() != null) {
try {
node.setCountryId(IsoCode.valueOf(result.getAddress().getCountryCode()));
} catch (IllegalArgumentException e) {
logger.warn("Unknown country code: {}", result.getAddress().getCountryCode());
}
}
fuzzySuccessful++;
logger.info("Fuzzy search successful for: {} (score: {})",
node.getName(), result.getScore());
} else {
logger.warn("Fuzzy search returned low confidence result for: {} (score: {})",
node.getName(), result.getScore());
}
} else {
logger.error("Fuzzy search found no results for: {}", node.getName());
}
}
logger.info("Fuzzy lookup recovered {} of {} failed addresses",
fuzzySuccessful, failedGeoLookups.size());
// Throw error for remaining failed lookups
int remainingFailed = failedGeoLookups.size() - fuzzySuccessful;
if (remainingFailed > 0) {
var firstFailed = failedGeoLookups.stream()
.filter(i -> i.getEntity().getGeoLat() == null)
.findFirst()
.map(BulkInstruction::getEntity)
.orElse(null);
if (firstFailed != null) {
throw new ExcelValidationError("Unable to geocode " + firstFailed.getName()
+ ". Please check your address or enter geo position yourself.");
}
}
}
}
private Optional<FuzzySearchResponse> executeFuzzySearch(ExcelNode node) {
try {
String companyName = node.getName();
String country = node.getCountryId().name();
// Normalisiere Unicode für konsistente Suche
companyName = java.text.Normalizer.normalize(companyName, java.text.Normalizer.Form.NFC);
// URL-Encoding
String encodedQuery = URLEncoder.encode(companyName + ", " + node.getAddress() + ", " + country, StandardCharsets.UTF_8);
String url = String.format(
"https://atlas.microsoft.com/search/fuzzy/json?api-version=1.0&subscription-key=%s&query=%s&limit=5",
subscriptionKey,
encodedQuery
);
URI uri = URI.create(url);
logger.debug("Fuzzy search for: {} (normalized & encoded)", companyName);
ResponseEntity<FuzzySearchResponse> response = restTemplate.getForEntity(
uri,
FuzzySearchResponse.class
);
return Optional.ofNullable(response.getBody());
} catch (Exception e) {
logger.error("Fuzzy search failed for {}", node.getName(), e);
return Optional.empty();
}
}
private String getGeoCodeString(ExcelNode excelNode) {

View file

@ -20,6 +20,7 @@ import de.avatic.lcc.repositories.premise.PremiseRepository;
import de.avatic.lcc.repositories.premise.RouteRepository;
import de.avatic.lcc.repositories.premise.RouteSectionRepository;
import de.avatic.lcc.repositories.properties.PropertyRepository;
import de.avatic.lcc.service.access.PropertyService;
import de.avatic.lcc.service.calculation.execution.steps.*;
import de.avatic.lcc.service.precalculation.PostCalculationCheckService;
import org.slf4j.Logger;
@ -51,9 +52,10 @@ public class CalculationExecutionService {
private final PostCalculationCheckService postCalculationCheckService;
private final CalculationJobDestinationRepository calculationJobDestinationRepository;
private final CalculationJobRouteSectionRepository calculationJobRouteSectionRepository;
private final PropertyService propertyService;
public CalculationExecutionService(PremiseRepository premiseRepository, DestinationRepository destinationRepository, RouteRepository routeRepository, RouteSectionRepository routeSectionRepository, CustomCostCalculationService customCostCalculationService, RouteSectionCostCalculationService routeSectionCostCalculationService, HandlingCostCalculationService handlingCostCalculationService, InventoryCostCalculationService inventoryCostCalculationService, PropertyRepository propertyRepository, AirfreightCalculationService airfreightCalculationService, PremiseToHuService premiseToHuService, ContainerCalculationService containerCalculationService, ShippingFrequencyCalculationService shippingFrequencyCalculationService, PostCalculationCheckService postCalculationCheckService, CalculationJobDestinationRepository calculationJobDestinationRepository, CalculationJobRouteSectionRepository calculationJobRouteSectionRepository) {
public CalculationExecutionService(PremiseRepository premiseRepository, DestinationRepository destinationRepository, RouteRepository routeRepository, RouteSectionRepository routeSectionRepository, CustomCostCalculationService customCostCalculationService, RouteSectionCostCalculationService routeSectionCostCalculationService, HandlingCostCalculationService handlingCostCalculationService, InventoryCostCalculationService inventoryCostCalculationService, PropertyRepository propertyRepository, AirfreightCalculationService airfreightCalculationService, PremiseToHuService premiseToHuService, ContainerCalculationService containerCalculationService, ShippingFrequencyCalculationService shippingFrequencyCalculationService, PostCalculationCheckService postCalculationCheckService, CalculationJobDestinationRepository calculationJobDestinationRepository, CalculationJobRouteSectionRepository calculationJobRouteSectionRepository, PropertyService propertyService) {
this.premiseRepository = premiseRepository;
this.destinationRepository = destinationRepository;
this.routeRepository = routeRepository;
@ -70,6 +72,7 @@ public class CalculationExecutionService {
this.postCalculationCheckService = postCalculationCheckService;
this.calculationJobDestinationRepository = calculationJobDestinationRepository;
this.calculationJobRouteSectionRepository = calculationJobRouteSectionRepository;
this.propertyService = propertyService;
}
private static ContainerType getBestContainerType(Map<ContainerType, List<SectionInfo>> sectionResults) {
@ -142,12 +145,14 @@ public class CalculationExecutionService {
CalculationJobDestination destinationCalculationJob = new CalculationJobDestination();
boolean hasMainRun = true;
BigDecimal leadTime = null;
boolean isWeightExceeded = false;
if (destination.getD2d()) {
selectedContainerCalculation = containerCalculationService.doCalculation(setId, premiseToHuService.createHuFromPremise(premise), ContainerType.FEU, premise.getHuMixable(), premise.getHuStackable());
sections = List.of(new SectionInfo(null, routeSectionCostCalculationService.doD2dCalculation(setId, periodId, premise, destination, selectedContainerCalculation), selectedContainerCalculation));
leadTime = BigDecimal.valueOf(destination.getLeadTimeD2d());
usedContainerType = ContainerType.FEU;
isWeightExceeded = sections.getFirst().result().isWeightPrice();
} else {
var bestContainerTypeResult = getSectionsFromBestContainerType(setId, periodId, destination, premise);
sections = bestContainerTypeResult.sections;
@ -162,6 +167,17 @@ public class CalculationExecutionService {
s.result().setPreRun(false);
s.result().setPostRun(false);
});
var containerSections = sections.stream().filter(s -> s.section().getRateType() != RateType.MATRIX).toList();
if(containerSections.size() > 1) {
isWeightExceeded = containerSections.stream().anyMatch(s -> s.result().isWeightPrice());
} else {
isWeightExceeded = sections.getFirst().result().isWeightPrice();
}
} else {
isWeightExceeded = sections.stream().map(SectionInfo::result).filter(CalculationJobRouteSection::getMainRun).anyMatch(CalculationJobRouteSection::isWeightPrice);
}
selectedContainerCalculation = bestContainerTypeResult.selectedContainerCalculation;
@ -207,7 +223,7 @@ public class CalculationExecutionService {
destinationCalculationJob.setAnnualCustomCost(customCost.getAnnualCost());
destinationCalculationJob.setAnnualTransportationCost(sections.stream().map(SectionInfo::result).map(CalculationJobRouteSection::getAnnualCost).reduce(BigDecimal.ZERO, BigDecimal::add));
destinationCalculationJob.setTransportWeightExceeded(sections.stream().map(SectionInfo::result).filter(CalculationJobRouteSection::getMainRun).anyMatch(CalculationJobRouteSection::isWeightPrice));
destinationCalculationJob.setTransportWeightExceeded(isWeightExceeded);
destinationCalculationJob.setLayerCount(sections.getFirst().containerResult().getLayer());
destinationCalculationJob.setLayerStructure(null); //TODO generate layer structure
destinationCalculationJob.setHuCount(sections.getFirst().containerResult().getHuUnitCount());
@ -216,10 +232,11 @@ public class CalculationExecutionService {
double huAnnualAmount = BigDecimal.valueOf(destination.getAnnualAmount()).divide(BigDecimal.valueOf(premise.getHuUnitCount()),4, RoundingMode.UP ).doubleValue();
destinationCalculationJob.setShippingFrequency(Double.valueOf(shippingFrequencyCalculationService.doCalculation(setId, huAnnualAmount, selectedContainerCalculation.getHuPerContainer(),!premise.getHuMixable())).intValue());
destinationCalculationJob.setShippingFrequency((int) Math.round(shippingFrequencyCalculationService.doCalculation(setId, huAnnualAmount, selectedContainerCalculation.getHuPerContainer(), !premise.getHuMixable())));
var commonCost = destinationCalculationJob.getAnnualHandlingCost()
.add(destinationCalculationJob.getAnnualDisposalCost())
.add(destinationCalculationJob.getAnnualRepackingCost())
.add(destinationCalculationJob.getAnnualCapitalCost())
.add(destinationCalculationJob.getAnnualStorageCost())
.add(materialCost.multiply(BigDecimal.valueOf(destination.getAnnualAmount())))
@ -245,6 +262,16 @@ public class CalculationExecutionService {
private BestContainerTypeResult getSectionsFromBestContainerType(Integer setId, Integer periodId, Destination destination, Premise premise) {
PackagingDimension hu = premiseToHuService.createHuFromPremise(premise);
Map<ContainerType, Boolean> active = new HashMap<>() {
{
put(ContainerType.TRUCK, true);
put(ContainerType.FEU, (Boolean)propertyService.getProperty(SystemPropertyMappingId.FEU, setId).orElse(true));
put(ContainerType.TEU, (Boolean)propertyService.getProperty(SystemPropertyMappingId.TEU, setId).orElse(true));
put(ContainerType.HC, (Boolean)propertyService.getProperty(SystemPropertyMappingId.FEU_HQ, setId).orElse(true));
}
};
var route = routeRepository.getSelectedByDestinationId(destination.getId()).orElseThrow();
List<RouteSection> routeSections = routeSectionRepository.getByRouteId(route.getId());
@ -253,11 +280,12 @@ public class CalculationExecutionService {
// Get container calculation
for (var containerType : ContainerType.values()) {
if (!active.get(containerType)) continue;
containerCalculation.put(containerType, containerCalculationService.doCalculation(setId, hu, containerType, premise.getHuMixable(), premise.getHuStackable()));
}
for (var containerType : ContainerType.values()) {
if (!containerType.equals(ContainerType.TRUCK)) {
if (!containerType.equals(ContainerType.TRUCK) && active.get(containerType)) {
var sectionInfo = new ArrayList<SectionInfo>();

View file

@ -140,26 +140,51 @@ public class NodeExcelMapper {
validateConstraints(row);
entity.setExternalMappingId(row.getCell(NodeHeader.MAPPING_ID.ordinal()).getStringCellValue());
entity.setName(row.getCell(NodeHeader.NAME.ordinal()).getStringCellValue());
entity.setAddress(row.getCell(NodeHeader.ADDRESS.ordinal()).getStringCellValue());
entity.setCountryId(IsoCode.valueOf(row.getCell(NodeHeader.COUNTRY.ordinal()).getStringCellValue()));
entity.setExternalMappingId(getCellValueAsString(row.getCell(NodeHeader.MAPPING_ID.ordinal())));
entity.setName(getCellValueAsString(row.getCell(NodeHeader.NAME.ordinal())));
entity.setAddress(getCellValueAsString(row.getCell(NodeHeader.ADDRESS.ordinal())));
entity.setCountryId(IsoCode.valueOf(getCellValueAsString(row.getCell(NodeHeader.COUNTRY.ordinal()))));
entity.setGeoLat(mapGeoCoordinate(CellUtil.getCell(row, NodeHeader.GEO_LATITUDE.ordinal())));
entity.setGeoLng(mapGeoCoordinate(CellUtil.getCell(row, NodeHeader.GEO_LONGITUDE.ordinal())));
entity.setSource(Boolean.valueOf(row.getCell(NodeHeader.IS_SOURCE.ordinal()).getStringCellValue()));
entity.setIntermediate(Boolean.valueOf(row.getCell(NodeHeader.IS_INTERMEDIATE.ordinal()).getStringCellValue()));
entity.setDestination(Boolean.valueOf(row.getCell(NodeHeader.IS_DESTINATION.ordinal()).getStringCellValue()));
entity.setSource(Boolean.valueOf(getCellValueAsString(row.getCell(NodeHeader.IS_SOURCE.ordinal()))));
entity.setIntermediate(Boolean.valueOf(getCellValueAsString(row.getCell(NodeHeader.IS_INTERMEDIATE.ordinal()))));
entity.setDestination(Boolean.valueOf(getCellValueAsString(row.getCell(NodeHeader.IS_DESTINATION.ordinal()))));
if(!entity.getSource() && !entity.getDestination() && !entity.getIntermediate())
throw new ExcelValidationError("Unable to validate row " + (row.getRowNum() + 1) + " column " + toExcelLetter(ContainerRateHeader.FROM_NODE.ordinal()) + ": Node with mapping id " + row.getCell(NodeHeader.MAPPING_ID.ordinal()).getStringCellValue() + " must be either source, destination or intermediate");
throw new ExcelValidationError("Unable to validate row " + (row.getRowNum() + 1) + " column " + toExcelLetter(ContainerRateHeader.FROM_NODE.ordinal()) + ": Node with mapping id " + getCellValueAsString(row.getCell(NodeHeader.MAPPING_ID.ordinal())) + " must be either source, destination or intermediate");
entity.setPredecessorRequired(Boolean.valueOf(getCellValueAsString(row.getCell(NodeHeader.IS_PREDECESSOR_MANDATORY.ordinal()))));
entity.setNodePredecessors(mapChainsFromCell(getCellValueAsString(CellUtil.getCell(row, NodeHeader.PREDECESSOR_NODES.ordinal()))));
entity.setOutboundCountries(mapOutboundCountriesFromCell(getCellValueAsString(CellUtil.getCell(row, NodeHeader.OUTBOUND_COUNTRIES.ordinal()))));
entity.setPredecessorRequired(Boolean.valueOf(row.getCell(NodeHeader.IS_PREDECESSOR_MANDATORY.ordinal()).getStringCellValue()));
entity.setNodePredecessors(mapChainsFromCell(CellUtil.getCell(row, NodeHeader.PREDECESSOR_NODES.ordinal()).getStringCellValue()));
entity.setOutboundCountries(mapOutboundCountriesFromCell(CellUtil.getCell(row, NodeHeader.OUTBOUND_COUNTRIES.ordinal()).getStringCellValue()));
return new BulkInstruction<>(entity, BulkInstructionType.valueOf(row.getCell(NodeHeader.OPERATION.ordinal()).getStringCellValue()));
return new BulkInstruction<>(entity, BulkInstructionType.valueOf(getCellValueAsString(row.getCell(NodeHeader.OPERATION.ordinal()))));
}
/**
* Extracts string value from cell with proper handling of different cell types and encoding
*/
private String getCellValueAsString(Cell cell) {
if (cell == null) {
return null;
}
return switch (cell.getCellType()) {
case STRING -> {
String value = cell.getStringCellValue();
yield java.text.Normalizer.normalize(value, java.text.Normalizer.Form.NFC).trim();
}
case NUMERIC -> {
if (DateUtil.isCellDateFormatted(cell)) {
yield cell.getDateCellValue().toString();
}
yield String.valueOf(cell.getNumericCellValue());
}
case BOOLEAN -> String.valueOf(cell.getBooleanCellValue());
case FORMULA -> cell.getCellFormula();
default -> "";
};
}