diff --git a/src/test/java/de/avatic/lcc/repositories/calculation/CalculationJobDestinationRepositoryIntegrationTest.java b/src/test/java/de/avatic/lcc/repositories/calculation/CalculationJobDestinationRepositoryIntegrationTest.java
new file mode 100644
index 0000000..8571a4d
--- /dev/null
+++ b/src/test/java/de/avatic/lcc/repositories/calculation/CalculationJobDestinationRepositoryIntegrationTest.java
@@ -0,0 +1,461 @@
+package de.avatic.lcc.repositories.calculation;
+
+import de.avatic.lcc.dto.generic.ContainerType;
+import de.avatic.lcc.model.db.calculations.CalculationJobDestination;
+import de.avatic.lcc.model.db.calculations.CalculationJobPriority;
+import de.avatic.lcc.model.db.calculations.CalculationJobState;
+import de.avatic.lcc.model.db.premises.PremiseState;
+import de.avatic.lcc.repositories.AbstractRepositoryIntegrationTest;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Integration tests for CalculationJobDestinationRepository.
+ *
+ * Tests critical functionality across both MySQL and MSSQL:
+ * - Complex entity with many BigDecimal fields
+ * - Enum handling (ContainerType)
+ * - Boolean fields
+ * - NULL handling for optional fields
+ * - Large INSERT statements
+ *
+ * Run with:
+ *
+ * mvn test -Dspring.profiles.active=test,mysql -Dtest=CalculationJobDestinationRepositoryIntegrationTest
+ * mvn test -Dspring.profiles.active=test,mssql -Dtest=CalculationJobDestinationRepositoryIntegrationTest
+ *
+ */
+class CalculationJobDestinationRepositoryIntegrationTest extends AbstractRepositoryIntegrationTest {
+
+ @Autowired
+ private CalculationJobDestinationRepository calculationJobDestinationRepository;
+
+ private Integer testUserId;
+ private Integer testCountryId;
+ private Integer testNodeId;
+ private Integer testMaterialId;
+ private Integer testPremiseId;
+ private Integer testDestinationId;
+ private Integer testValidityPeriodId;
+ private Integer testPropertySetId;
+ private Integer testCalculationJobId;
+
+ @BeforeEach
+ void setupTestData() {
+ // Clean up in correct order
+ jdbcTemplate.update("DELETE FROM calculation_job_route_section");
+ jdbcTemplate.update("DELETE FROM calculation_job_destination");
+ jdbcTemplate.update("DELETE FROM calculation_job");
+ jdbcTemplate.update("DELETE FROM premise_route_section");
+ jdbcTemplate.update("DELETE FROM premise_route_node");
+ jdbcTemplate.update("DELETE FROM premise_route");
+ jdbcTemplate.update("DELETE FROM premise_destination");
+ jdbcTemplate.update("DELETE FROM premise");
+ jdbcTemplate.update("DELETE FROM material");
+
+ // Clean up node-referencing tables
+ jdbcTemplate.update("DELETE FROM container_rate");
+ jdbcTemplate.update("DELETE FROM country_matrix_rate");
+ jdbcTemplate.update("DELETE FROM node_predecessor_entry");
+ jdbcTemplate.update("DELETE FROM node_predecessor_chain");
+ jdbcTemplate.update("DELETE FROM distance_matrix");
+
+ jdbcTemplate.update("DELETE FROM node");
+ jdbcTemplate.update("DELETE FROM sys_user");
+
+ // Clean up validity_period referencing tables
+ jdbcTemplate.update("DELETE FROM country_property");
+ jdbcTemplate.update("DELETE FROM validity_period");
+
+ // Clean up property_set referencing tables
+ jdbcTemplate.update("DELETE FROM system_property");
+ jdbcTemplate.update("DELETE FROM property_set");
+
+ // Create test user
+ testUserId = createUser("WD001", "test@example.com");
+
+ // Get test country
+ testCountryId = getCountryId("DE");
+
+ // Create test node
+ testNodeId = createNode("Test Node", "NODE-001", testCountryId);
+
+ // Create test material
+ testMaterialId = createMaterial("Test Material", "MAT-001");
+
+ // Create test validity period
+ testValidityPeriodId = createValidityPeriod("VALID");
+
+ // Create test property set
+ testPropertySetId = createPropertySet("VALID");
+
+ // Create test premise
+ testPremiseId = createPremise(testUserId, testNodeId, testMaterialId, testCountryId, PremiseState.COMPLETED);
+
+ // Create test destination
+ testDestinationId = createDestination(testPremiseId, testNodeId);
+
+ // Create test calculation job
+ testCalculationJobId = createCalculationJob(testPremiseId, testValidityPeriodId, testPropertySetId, testUserId);
+ }
+
+ @Test
+ void testInsertAndGetByJobId() {
+ // Given: New calculation job destination
+ CalculationJobDestination destination = createFullCalculationJobDestination();
+
+ // When: Insert
+ Integer id = calculationJobDestinationRepository.insert(destination);
+
+ // Then: Should be inserted
+ assertNotNull(id);
+ assertTrue(id > 0);
+
+ // And: Should be retrievable by job ID
+ List destinations =
+ calculationJobDestinationRepository.getDestinationsByJobId(testCalculationJobId);
+
+ assertEquals(1, destinations.size());
+ CalculationJobDestination retrieved = destinations.get(0);
+
+ assertEquals(testCalculationJobId, retrieved.getCalculationJobId());
+ assertEquals(testDestinationId, retrieved.getPremiseDestinationId());
+ assertEquals(ContainerType.FEU, retrieved.getContainerType());
+ assertEquals(10, retrieved.getShippingFrequency());
+ assertTrue(retrieved.getSmallUnit());
+ assertFalse(retrieved.getTransportWeightExceeded());
+ }
+
+ @Test
+ void testGetDestinationsByJobIdEmpty() {
+ // When: Get destinations for job with no destinations
+ List destinations =
+ calculationJobDestinationRepository.getDestinationsByJobId(testCalculationJobId);
+
+ // Then: Should return empty list
+ assertNotNull(destinations);
+ assertTrue(destinations.isEmpty());
+ }
+
+ @Test
+ void testGetDestinationsByJobIdMultiple() {
+ // Given: Multiple destinations for same job
+ CalculationJobDestination dest1 = createFullCalculationJobDestination();
+ dest1.setContainerType(ContainerType.FEU);
+
+ CalculationJobDestination dest2 = createFullCalculationJobDestination();
+ dest2.setContainerType(ContainerType.TEU);
+
+ CalculationJobDestination dest3 = createFullCalculationJobDestination();
+ dest3.setContainerType(ContainerType.HC);
+
+ // When: Insert all
+ calculationJobDestinationRepository.insert(dest1);
+ calculationJobDestinationRepository.insert(dest2);
+ calculationJobDestinationRepository.insert(dest3);
+
+ // Then: Should retrieve all for job
+ List destinations =
+ calculationJobDestinationRepository.getDestinationsByJobId(testCalculationJobId);
+
+ assertEquals(3, destinations.size());
+ }
+
+ @Test
+ void testContainerTypeEnum() {
+ // Given: Destinations with different container types
+ CalculationJobDestination dest1 = createFullCalculationJobDestination();
+ dest1.setContainerType(ContainerType.FEU);
+
+ CalculationJobDestination dest2 = createFullCalculationJobDestination();
+ dest2.setContainerType(ContainerType.TEU);
+
+ CalculationJobDestination dest3 = createFullCalculationJobDestination();
+ dest3.setContainerType(ContainerType.TRUCK);
+
+ // When: Insert
+ calculationJobDestinationRepository.insert(dest1);
+ calculationJobDestinationRepository.insert(dest2);
+ calculationJobDestinationRepository.insert(dest3);
+
+ // Then: Container types should be stored correctly
+ List destinations =
+ calculationJobDestinationRepository.getDestinationsByJobId(testCalculationJobId);
+
+ assertTrue(destinations.stream().anyMatch(d -> d.getContainerType() == ContainerType.FEU));
+ assertTrue(destinations.stream().anyMatch(d -> d.getContainerType() == ContainerType.TEU));
+ assertTrue(destinations.stream().anyMatch(d -> d.getContainerType() == ContainerType.TRUCK));
+ }
+
+ @Test
+ void testBooleanFields() {
+ // Given: Destination with specific boolean values
+ CalculationJobDestination destination = createFullCalculationJobDestination();
+ destination.setSmallUnit(true);
+ destination.setTransportWeightExceeded(false);
+ destination.setD2D(true);
+
+ // When: Insert
+ Integer id = calculationJobDestinationRepository.insert(destination);
+
+ // Then: Boolean values should be stored correctly
+ List destinations =
+ calculationJobDestinationRepository.getDestinationsByJobId(testCalculationJobId);
+
+ assertEquals(1, destinations.size());
+ CalculationJobDestination retrieved = destinations.get(0);
+
+ assertTrue(retrieved.getSmallUnit());
+ assertFalse(retrieved.getTransportWeightExceeded());
+ }
+
+ @Test
+ void testBigDecimalFields() {
+ // Given: Destination with specific decimal values
+ CalculationJobDestination destination = createFullCalculationJobDestination();
+ destination.setTotalCost(new BigDecimal("12345.67"));
+ destination.setAnnualAmount(new BigDecimal("50000.00"));
+ destination.setAnnualTransportationCost(new BigDecimal("8500.50"));
+ destination.setContainerUtilization(new BigDecimal("0.85"));
+
+ // When: Insert
+ calculationJobDestinationRepository.insert(destination);
+
+ // Then: Decimal values should be stored correctly
+ List destinations =
+ calculationJobDestinationRepository.getDestinationsByJobId(testCalculationJobId);
+
+ assertEquals(1, destinations.size());
+ CalculationJobDestination retrieved = destinations.get(0);
+
+ assertEquals(0, new BigDecimal("12345.67").compareTo(retrieved.getTotalCost()));
+ assertEquals(0, new BigDecimal("50000.00").compareTo(retrieved.getAnnualAmount()));
+ assertEquals(0, new BigDecimal("8500.50").compareTo(retrieved.getAnnualTransportationCost()));
+ assertEquals(0, new BigDecimal("0.85").compareTo(retrieved.getContainerUtilization()));
+ }
+
+ @Test
+ void testNullableFields() {
+ // Given: Destination with some nullable fields as null
+ CalculationJobDestination destination = createMinimalCalculationJobDestination();
+
+ // When: Insert
+ Integer id = calculationJobDestinationRepository.insert(destination);
+
+ // Then: Should be inserted successfully
+ assertNotNull(id);
+
+ List destinations =
+ calculationJobDestinationRepository.getDestinationsByJobId(testCalculationJobId);
+
+ assertEquals(1, destinations.size());
+ }
+
+ // ========== Helper Methods ==========
+
+ private CalculationJobDestination createFullCalculationJobDestination() {
+ CalculationJobDestination destination = new CalculationJobDestination();
+
+ // Core identifiers
+ destination.setCalculationJobId(testCalculationJobId);
+ destination.setPremiseDestinationId(testDestinationId);
+ destination.setShippingFrequency(10);
+ destination.setTotalCost(new BigDecimal("10000.00"));
+ destination.setAnnualAmount(new BigDecimal("50000.00"));
+
+ // Risk calculations
+ destination.setTotalRiskCost(new BigDecimal("500.00"));
+ destination.setTotalChanceCost(new BigDecimal("300.00"));
+
+ // Handling costs
+ destination.setSmallUnit(true);
+ destination.setAnnualRepackingCost(new BigDecimal("200.00"));
+ destination.setAnnualHandlingCost(new BigDecimal("150.00"));
+ destination.setAnnualDisposalCost(new BigDecimal("100.00"));
+
+ // Inventory management
+ destination.setOperationalStock(new BigDecimal("1000.00"));
+ destination.setSafetyStock(new BigDecimal("500.00"));
+ destination.setStockedInventory(new BigDecimal("1500.00"));
+ destination.setInTransportStock(new BigDecimal("300.00"));
+ destination.setStockBeforePayment(new BigDecimal("800.00"));
+ destination.setAnnualCapitalCost(new BigDecimal("250.00"));
+ destination.setAnnualStorageCost(new BigDecimal("400.00"));
+
+ // Customs
+ destination.setCustomValue(new BigDecimal("5000.00"));
+ destination.setCustomDuties(new BigDecimal("750.00"));
+ destination.setTariffRate(new BigDecimal("0.15"));
+ destination.setAnnualCustomCost(new BigDecimal("900.00"));
+
+ // Air freight risk
+ destination.setAirFreightShareMax(new BigDecimal("0.20"));
+ destination.setAirFreightShare(new BigDecimal("0.10"));
+ destination.setAirFreightVolumetricWeight(new BigDecimal("150.00"));
+ destination.setAirFreightWeight(new BigDecimal("120.00"));
+ destination.setAnnualAirFreightCost(new BigDecimal("1200.00"));
+
+ // Transportation
+ destination.setContainerType(ContainerType.FEU);
+ destination.setHuCount(20);
+ destination.setLayerStructure(null); // JSON column, set to null (TODO in production code)
+ destination.setLayerCount(3);
+ destination.setTransportWeightExceeded(false);
+ destination.setAnnualTransportationCost(new BigDecimal("8000.00"));
+ destination.setContainerUtilization(new BigDecimal("0.85"));
+ destination.setTotalTransitTime(30);
+ destination.setSafetyStockInDays(new BigDecimal("10.00"));
+
+ // Material costs
+ destination.setMaterialCost(new BigDecimal("50.00"));
+ destination.setFcaCost(new BigDecimal("55.00"));
+
+ destination.setD2D(false);
+ destination.setRateD2D(new BigDecimal("0.00"));
+
+ return destination;
+ }
+
+ private CalculationJobDestination createMinimalCalculationJobDestination() {
+ CalculationJobDestination destination = new CalculationJobDestination();
+
+ // Only required fields
+ destination.setCalculationJobId(testCalculationJobId);
+ destination.setPremiseDestinationId(testDestinationId);
+ destination.setShippingFrequency(5);
+ destination.setTotalCost(new BigDecimal("5000.00"));
+ destination.setAnnualAmount(new BigDecimal("25000.00"));
+ destination.setTotalRiskCost(new BigDecimal("0.00"));
+ destination.setTotalChanceCost(new BigDecimal("0.00"));
+ destination.setSmallUnit(false);
+ destination.setAnnualRepackingCost(new BigDecimal("0.00"));
+ destination.setAnnualHandlingCost(new BigDecimal("0.00"));
+ destination.setAnnualDisposalCost(new BigDecimal("0.00"));
+ destination.setOperationalStock(new BigDecimal("0.00"));
+ destination.setSafetyStock(new BigDecimal("0.00"));
+ destination.setStockedInventory(new BigDecimal("0.00"));
+ destination.setInTransportStock(new BigDecimal("0.00"));
+ destination.setStockBeforePayment(new BigDecimal("0.00"));
+ destination.setAnnualCapitalCost(new BigDecimal("0.00"));
+ destination.setAnnualStorageCost(new BigDecimal("0.00"));
+ destination.setCustomValue(new BigDecimal("0.00"));
+ destination.setCustomDuties(new BigDecimal("0.00"));
+ destination.setTariffRate(new BigDecimal("0.00"));
+ destination.setAnnualCustomCost(new BigDecimal("0.00"));
+ destination.setAirFreightShareMax(new BigDecimal("0.00"));
+ destination.setAirFreightShare(new BigDecimal("0.00"));
+ destination.setAirFreightVolumetricWeight(new BigDecimal("0.00"));
+ destination.setAirFreightWeight(new BigDecimal("0.00"));
+ destination.setAnnualAirFreightCost(new BigDecimal("0.00"));
+ destination.setContainerType(ContainerType.FEU);
+ destination.setHuCount(10);
+ destination.setLayerStructure(null); // JSON column, set to null
+ destination.setLayerCount(2);
+ destination.setTransportWeightExceeded(false);
+ destination.setAnnualTransportationCost(new BigDecimal("0.00"));
+ destination.setContainerUtilization(new BigDecimal("0.50"));
+ destination.setTotalTransitTime(15);
+ destination.setSafetyStockInDays(new BigDecimal("5.00"));
+ destination.setMaterialCost(new BigDecimal("0.00"));
+ destination.setFcaCost(new BigDecimal("0.00"));
+ destination.setD2D(false);
+ destination.setRateD2D(new BigDecimal("0.00"));
+
+ return destination;
+ }
+
+ private Integer createUser(String workdayId, String email) {
+ String sql = String.format(
+ "INSERT INTO sys_user (workday_id, email, firstname, lastname, is_active) VALUES (?, ?, ?, ?, %s)",
+ dialectProvider.getBooleanTrue());
+ executeRawSql(sql, workdayId, email, "Test", "User");
+
+ String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
+ return jdbcTemplate.queryForObject(selectSql, Integer.class);
+ }
+
+ private Integer getCountryId(String isoCode) {
+ return jdbcTemplate.queryForObject("SELECT id FROM country WHERE iso_code = ?", Integer.class, isoCode);
+ }
+
+ private Integer createNode(String name, String externalId, Integer countryId) {
+ String sql = String.format(
+ "INSERT INTO node (name, external_mapping_id, country_id, is_deprecated, is_source, is_destination, is_intermediate, address) " +
+ "VALUES (?, ?, ?, %s, %s, %s, %s, 'Test Address')",
+ dialectProvider.getBooleanFalse(),
+ dialectProvider.getBooleanTrue(),
+ dialectProvider.getBooleanTrue(),
+ dialectProvider.getBooleanTrue());
+ executeRawSql(sql, name, externalId, countryId);
+
+ String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
+ return jdbcTemplate.queryForObject(selectSql, Integer.class);
+ }
+
+ private Integer createMaterial(String name, String partNumber) {
+ String sql = String.format(
+ "INSERT INTO material (name, part_number, normalized_part_number, hs_code, is_deprecated) VALUES (?, ?, ?, '123456', %s)",
+ dialectProvider.getBooleanFalse());
+ executeRawSql(sql, name, partNumber, partNumber);
+
+ String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
+ return jdbcTemplate.queryForObject(selectSql, Integer.class);
+ }
+
+ private Integer createValidityPeriod(String state) {
+ String sql = String.format(
+ "INSERT INTO validity_period (state, start_date) VALUES (?, %s)",
+ isMysql() ? "NOW()" : "GETDATE()");
+ executeRawSql(sql, state);
+
+ String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
+ return jdbcTemplate.queryForObject(selectSql, Integer.class);
+ }
+
+ private Integer createPropertySet(String state) {
+ String sql = String.format(
+ "INSERT INTO property_set (state, start_date) VALUES (?, %s)",
+ isMysql() ? "NOW()" : "GETDATE()");
+ executeRawSql(sql, state);
+
+ String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
+ return jdbcTemplate.queryForObject(selectSql, Integer.class);
+ }
+
+ private Integer createPremise(Integer userId, Integer nodeId, Integer materialId, Integer countryId, PremiseState state) {
+ String sql = String.format(
+ "INSERT INTO premise (user_id, supplier_node_id, material_id, country_id, state, geo_lat, geo_lng, created_at, updated_at) " +
+ "VALUES (?, ?, ?, ?, ?, 51.5, 7.5, %s, %s)",
+ isMysql() ? "NOW()" : "GETDATE()",
+ isMysql() ? "NOW()" : "GETDATE()");
+ executeRawSql(sql, userId, nodeId, materialId, countryId, state.name());
+
+ String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
+ return jdbcTemplate.queryForObject(selectSql, Integer.class);
+ }
+
+ private Integer createDestination(Integer premiseId, Integer nodeId) {
+ String sql = "INSERT INTO premise_destination (premise_id, destination_node_id, annual_amount, country_id, geo_lat, geo_lng) " +
+ "VALUES (?, ?, 1000, ?, 51.5, 7.5)";
+ executeRawSql(sql, premiseId, nodeId, testCountryId);
+
+ String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
+ return jdbcTemplate.queryForObject(selectSql, Integer.class);
+ }
+
+ private Integer createCalculationJob(Integer premiseId, Integer validityPeriodId, Integer propertySetId, Integer userId) {
+ String sql = "INSERT INTO calculation_job (premise_id, validity_period_id, property_set_id, user_id, job_state, priority, retries) " +
+ "VALUES (?, ?, ?, ?, ?, ?, 0)";
+ executeRawSql(sql, premiseId, validityPeriodId, propertySetId, userId,
+ CalculationJobState.VALID.name(), CalculationJobPriority.MEDIUM.name());
+
+ String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
+ return jdbcTemplate.queryForObject(selectSql, Integer.class);
+ }
+}
diff --git a/src/test/java/de/avatic/lcc/repositories/calculation/CalculationJobRouteSectionRepositoryIntegrationTest.java b/src/test/java/de/avatic/lcc/repositories/calculation/CalculationJobRouteSectionRepositoryIntegrationTest.java
new file mode 100644
index 0000000..12e3313
--- /dev/null
+++ b/src/test/java/de/avatic/lcc/repositories/calculation/CalculationJobRouteSectionRepositoryIntegrationTest.java
@@ -0,0 +1,523 @@
+package de.avatic.lcc.repositories.calculation;
+
+import de.avatic.lcc.dto.generic.ContainerType;
+import de.avatic.lcc.dto.generic.RateType;
+import de.avatic.lcc.dto.generic.TransportType;
+import de.avatic.lcc.model.db.calculations.CalculationJobPriority;
+import de.avatic.lcc.model.db.calculations.CalculationJobRouteSection;
+import de.avatic.lcc.model.db.calculations.CalculationJobState;
+import de.avatic.lcc.model.db.premises.PremiseState;
+import de.avatic.lcc.repositories.AbstractRepositoryIntegrationTest;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class CalculationJobRouteSectionRepositoryIntegrationTest extends AbstractRepositoryIntegrationTest {
+
+ @Autowired
+ private CalculationJobRouteSectionRepository repository;
+
+ private Integer testUserId;
+ private Integer testCountryId;
+ private Integer testNodeId;
+ private Integer testMaterialId;
+ private Integer testValidityPeriodId;
+ private Integer testPropertySetId;
+ private Integer testPremiseId;
+ private Integer testCalculationJobDestinationId1;
+ private Integer testCalculationJobDestinationId2;
+ private Integer testPremiseRouteSectionId;
+
+ @BeforeEach
+ void setupTestData() {
+ // Clean up calculation job dependent tables
+ jdbcTemplate.update("DELETE FROM calculation_job_route_section");
+ jdbcTemplate.update("DELETE FROM calculation_job_destination");
+ jdbcTemplate.update("DELETE FROM calculation_job");
+
+ // Clean up premise dependent tables
+ jdbcTemplate.update("DELETE FROM premise_route_section");
+ jdbcTemplate.update("DELETE FROM premise_route");
+ jdbcTemplate.update("DELETE FROM premise_route_node");
+ jdbcTemplate.update("DELETE FROM premise_destination");
+ jdbcTemplate.update("DELETE FROM premise");
+ jdbcTemplate.update("DELETE FROM packaging");
+ jdbcTemplate.update("DELETE FROM material");
+
+ // Clean up node-referencing tables
+ jdbcTemplate.update("DELETE FROM container_rate");
+ jdbcTemplate.update("DELETE FROM country_matrix_rate");
+ jdbcTemplate.update("DELETE FROM node_predecessor_entry");
+ jdbcTemplate.update("DELETE FROM node_predecessor_chain");
+ jdbcTemplate.update("DELETE FROM distance_matrix");
+
+ jdbcTemplate.update("DELETE FROM node");
+ jdbcTemplate.update("DELETE FROM sys_user");
+
+ // Clean up validity_period referencing tables
+ jdbcTemplate.update("DELETE FROM country_property");
+ jdbcTemplate.update("DELETE FROM validity_period");
+
+ // Clean up property_set referencing tables
+ jdbcTemplate.update("DELETE FROM system_property");
+ jdbcTemplate.update("DELETE FROM property_set");
+
+ // Create test user
+ testUserId = createUser("WD001", "test@example.com");
+
+ // Get test country
+ testCountryId = getCountryId("DE");
+
+ // Create test node
+ testNodeId = createNode("Test Node", "NODE-001", testCountryId);
+
+ // Create test material
+ testMaterialId = createMaterial("Test Material", "MAT-001");
+
+ // Create test validity period
+ testValidityPeriodId = createValidityPeriod("VALID");
+
+ // Create test property set
+ testPropertySetId = createPropertySet("VALID");
+
+ // Create test premise
+ testPremiseId = createPremise(testUserId, testNodeId, testMaterialId, testCountryId, PremiseState.COMPLETED);
+
+ // Create premise destination
+ Integer destinationId = createDestination(testPremiseId, testNodeId);
+
+ // Create test calculation job
+ Integer calculationJobId = createCalculationJob(testPremiseId, testValidityPeriodId, testPropertySetId, testUserId);
+
+ // Create test calculation job destinations (minimal)
+ testCalculationJobDestinationId1 = createMinimalCalculationJobDestination(calculationJobId, testPremiseId, destinationId);
+ testCalculationJobDestinationId2 = createMinimalCalculationJobDestination(calculationJobId, testPremiseId, destinationId);
+
+ // Create test premise route section
+ Integer routeId = createRoute(destinationId);
+ Integer fromNodeId = createRouteNode(testNodeId, testCountryId);
+ Integer toNodeId = createRouteNode(testNodeId, testCountryId);
+ testPremiseRouteSectionId = createRouteSection(routeId, fromNodeId, toNodeId);
+ }
+
+ // ========== INSERT TESTS ==========
+
+ @Test
+ void testInsertWithAllFields() {
+ CalculationJobRouteSection section = createFullRouteSection();
+ section.setCalculationJobDestinationId(testCalculationJobDestinationId1);
+ section.setPremiseRouteSectionId(testPremiseRouteSectionId);
+
+ Integer id = repository.insert(section);
+
+ assertNotNull(id);
+ assertTrue(id > 0);
+
+ List sections = repository.getRouteSectionsByDestinationId(testCalculationJobDestinationId1);
+ assertEquals(1, sections.size());
+
+ CalculationJobRouteSection retrieved = sections.get(0);
+ assertEquals(id, retrieved.getId());
+ assertEquals(testPremiseRouteSectionId, retrieved.getPremiseRouteSectionId());
+ assertEquals(testCalculationJobDestinationId1, retrieved.getCalculationJobDestinationId());
+ assertEquals(TransportType.SEA, retrieved.getTransportType());
+ assertEquals(RateType.CONTAINER, retrieved.getRateType());
+ assertTrue(retrieved.getUnmixedPrice());
+ assertTrue(retrieved.isCbmPrice());
+ assertFalse(retrieved.isWeightPrice());
+ assertTrue(retrieved.getStacked());
+ assertFalse(retrieved.getPreRun());
+ assertTrue(retrieved.getMainRun());
+ assertFalse(retrieved.getPostRun());
+ assertEquals(0, new BigDecimal("500.00").compareTo(retrieved.getRate()));
+ assertEquals(0, new BigDecimal("1500.50").compareTo(retrieved.getDistance()));
+ assertEquals(0, new BigDecimal("100.00").compareTo(retrieved.getCbmPrice()));
+ assertEquals(0, new BigDecimal("80.00").compareTo(retrieved.getWeightPrice()));
+ assertEquals(0, new BigDecimal("25000.00").compareTo(retrieved.getAnnualCost()));
+ assertEquals(15, retrieved.getTransitTime());
+ }
+
+ @Test
+ void testInsertWithNullPremiseRouteSectionId() {
+ CalculationJobRouteSection section = createFullRouteSection();
+ section.setCalculationJobDestinationId(testCalculationJobDestinationId1);
+ section.setPremiseRouteSectionId(null); // Nullable field
+
+ Integer id = repository.insert(section);
+
+ assertNotNull(id);
+
+ List sections = repository.getRouteSectionsByDestinationId(testCalculationJobDestinationId1);
+ assertEquals(1, sections.size());
+ // Note: ResultSet.getInt() returns 0 for NULL, so we can't distinguish null from 0
+ assertEquals(0, sections.get(0).getPremiseRouteSectionId());
+ }
+
+ @Test
+ void testInsertWithMatrixRateType() {
+ CalculationJobRouteSection section = createMinimalRouteSection();
+ section.setCalculationJobDestinationId(testCalculationJobDestinationId1);
+ section.setTransportType(TransportType.ROAD);
+ section.setRateType(RateType.MATRIX);
+
+ Integer id = repository.insert(section);
+ assertNotNull(id);
+
+ List sections = repository.getRouteSectionsByDestinationId(testCalculationJobDestinationId1);
+ assertEquals(1, sections.size());
+
+ // MATRIX rate type is stored as "MATRIX" in transport_type column and converted back
+ assertEquals(TransportType.ROAD, sections.get(0).getTransportType());
+ assertEquals(RateType.MATRIX, sections.get(0).getRateType());
+ }
+
+ @Test
+ void testInsertWithD2DRateType() {
+ CalculationJobRouteSection section = createMinimalRouteSection();
+ section.setCalculationJobDestinationId(testCalculationJobDestinationId1);
+ section.setTransportType(TransportType.ROAD);
+ section.setRateType(RateType.D2D);
+
+ Integer id = repository.insert(section);
+ assertNotNull(id);
+
+ List sections = repository.getRouteSectionsByDestinationId(testCalculationJobDestinationId1);
+ assertEquals(1, sections.size());
+
+ // D2D rate type is stored as "D2D" in transport_type column and converted back
+ assertEquals(TransportType.ROAD, sections.get(0).getTransportType());
+ assertEquals(RateType.D2D, sections.get(0).getRateType());
+ }
+
+ @Test
+ void testInsertWithContainerRateType() {
+ CalculationJobRouteSection section = createMinimalRouteSection();
+ section.setCalculationJobDestinationId(testCalculationJobDestinationId1);
+ section.setTransportType(TransportType.RAIL);
+ section.setRateType(RateType.CONTAINER);
+
+ Integer id = repository.insert(section);
+ assertNotNull(id);
+
+ List sections = repository.getRouteSectionsByDestinationId(testCalculationJobDestinationId1);
+ assertEquals(1, sections.size());
+
+ // CONTAINER rate type stores the transport type directly
+ assertEquals(TransportType.RAIL, sections.get(0).getTransportType());
+ assertEquals(RateType.CONTAINER, sections.get(0).getRateType());
+ }
+
+ @Test
+ void testBooleanFlags() {
+ CalculationJobRouteSection section = createMinimalRouteSection();
+ section.setCalculationJobDestinationId(testCalculationJobDestinationId1);
+ section.setUnmixedPrice(true);
+ section.setCbmPrice(true);
+ section.setWeightPrice(false);
+ section.setStacked(false);
+ section.setPreRun(true);
+ section.setMainRun(false);
+ section.setPostRun(true);
+
+ Integer id = repository.insert(section);
+ assertNotNull(id);
+
+ List sections = repository.getRouteSectionsByDestinationId(testCalculationJobDestinationId1);
+ assertEquals(1, sections.size());
+
+ CalculationJobRouteSection retrieved = sections.get(0);
+ assertTrue(retrieved.getUnmixedPrice());
+ assertTrue(retrieved.isCbmPrice());
+ assertFalse(retrieved.isWeightPrice());
+ assertFalse(retrieved.getStacked());
+ assertTrue(retrieved.getPreRun());
+ assertFalse(retrieved.getMainRun());
+ assertTrue(retrieved.getPostRun());
+ }
+
+ // ========== QUERY TESTS ==========
+
+ @Test
+ void testGetRouteSectionsByDestinationId() {
+ CalculationJobRouteSection section1 = createFullRouteSection();
+ section1.setCalculationJobDestinationId(testCalculationJobDestinationId1);
+ repository.insert(section1);
+
+ CalculationJobRouteSection section2 = createFullRouteSection();
+ section2.setCalculationJobDestinationId(testCalculationJobDestinationId1);
+ section2.setTransportType(TransportType.RAIL);
+ repository.insert(section2);
+
+ CalculationJobRouteSection section3 = createFullRouteSection();
+ section3.setCalculationJobDestinationId(testCalculationJobDestinationId2);
+ repository.insert(section3);
+
+ List sections = repository.getRouteSectionsByDestinationId(testCalculationJobDestinationId1);
+
+ assertEquals(2, sections.size());
+ assertTrue(sections.stream().allMatch(s -> s.getCalculationJobDestinationId().equals(testCalculationJobDestinationId1)));
+ }
+
+ @Test
+ void testGetRouteSectionsByDestinationIdNotFound() {
+ List sections = repository.getRouteSectionsByDestinationId(99999);
+ assertTrue(sections.isEmpty());
+ }
+
+ @Test
+ void testGetRouteSectionsByDestinationIds() {
+ CalculationJobRouteSection section1 = createFullRouteSection();
+ section1.setCalculationJobDestinationId(testCalculationJobDestinationId1);
+ repository.insert(section1);
+
+ CalculationJobRouteSection section2 = createFullRouteSection();
+ section2.setCalculationJobDestinationId(testCalculationJobDestinationId1);
+ repository.insert(section2);
+
+ CalculationJobRouteSection section3 = createFullRouteSection();
+ section3.setCalculationJobDestinationId(testCalculationJobDestinationId2);
+ repository.insert(section3);
+
+ Map> grouped = repository.getRouteSectionsByDestinationIds(
+ List.of(testCalculationJobDestinationId1, testCalculationJobDestinationId2)
+ );
+
+ assertEquals(2, grouped.size());
+ assertTrue(grouped.containsKey(testCalculationJobDestinationId1));
+ assertTrue(grouped.containsKey(testCalculationJobDestinationId2));
+ assertEquals(2, grouped.get(testCalculationJobDestinationId1).size());
+ assertEquals(1, grouped.get(testCalculationJobDestinationId2).size());
+ }
+
+ @Test
+ void testGetRouteSectionsByDestinationIdsEmpty() {
+ Map> grouped = repository.getRouteSectionsByDestinationIds(List.of());
+ assertTrue(grouped.isEmpty());
+ }
+
+ @Test
+ void testGetRouteSectionsByDestinationIdsNull() {
+ Map> grouped = repository.getRouteSectionsByDestinationIds(null);
+ assertTrue(grouped.isEmpty());
+ }
+
+ @Test
+ void testGetRouteSectionsByDestinationIdsNotFound() {
+ Map> grouped = repository.getRouteSectionsByDestinationIds(
+ List.of(99998, 99999)
+ );
+ assertTrue(grouped.isEmpty());
+ }
+
+ // ========== HELPER METHODS ==========
+
+ private CalculationJobRouteSection createFullRouteSection() {
+ CalculationJobRouteSection section = new CalculationJobRouteSection();
+ section.setTransportType(TransportType.SEA);
+ section.setRateType(RateType.CONTAINER);
+ section.setUnmixedPrice(true);
+ section.setCbmPrice(true);
+ section.setWeightPrice(false);
+ section.setStacked(true);
+ section.setPreRun(false);
+ section.setMainRun(true);
+ section.setPostRun(false);
+ section.setRate(new BigDecimal("500.00"));
+ section.setDistance(new BigDecimal("1500.50"));
+ section.setCbmPrice(new BigDecimal("100.00"));
+ section.setWeightPrice(new BigDecimal("80.00"));
+ section.setAnnualCost(new BigDecimal("25000.00"));
+ section.setTransitTime(15);
+ return section;
+ }
+
+ private CalculationJobRouteSection createMinimalRouteSection() {
+ CalculationJobRouteSection section = new CalculationJobRouteSection();
+ section.setTransportType(TransportType.ROAD);
+ section.setRateType(RateType.MATRIX);
+ section.setUnmixedPrice(false);
+ section.setCbmPrice(false);
+ section.setWeightPrice(false);
+ section.setStacked(true); // Must satisfy constraint: is_unmixed_price IS TRUE OR is_stacked IS TRUE
+ section.setPreRun(false);
+ section.setMainRun(false);
+ section.setPostRun(false);
+ section.setRate(new BigDecimal("0.00"));
+ section.setDistance(new BigDecimal("0.00"));
+ section.setCbmPrice(new BigDecimal("0.00"));
+ section.setWeightPrice(new BigDecimal("0.00"));
+ section.setAnnualCost(new BigDecimal("0.00"));
+ section.setTransitTime(0);
+ return section;
+ }
+
+ private Integer createUser(String workdayId, String email) {
+ String sql = String.format(
+ "INSERT INTO sys_user (workday_id, email, firstname, lastname, is_active) VALUES (?, ?, ?, ?, %s)",
+ dialectProvider.getBooleanTrue());
+ executeRawSql(sql, workdayId, email, "Test", "User");
+
+ String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
+ return jdbcTemplate.queryForObject(selectSql, Integer.class);
+ }
+
+ private Integer getCountryId(String isoCode) {
+ return jdbcTemplate.queryForObject("SELECT id FROM country WHERE iso_code = ?", Integer.class, isoCode);
+ }
+
+ private Integer createNode(String name, String externalId, Integer countryId) {
+ String sql = String.format(
+ "INSERT INTO node (name, external_mapping_id, country_id, is_deprecated, is_source, is_destination, is_intermediate, address) " +
+ "VALUES (?, ?, ?, %s, %s, %s, %s, 'Test Address')",
+ dialectProvider.getBooleanFalse(),
+ dialectProvider.getBooleanTrue(),
+ dialectProvider.getBooleanTrue(),
+ dialectProvider.getBooleanTrue());
+ executeRawSql(sql, name, externalId, countryId);
+
+ String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
+ return jdbcTemplate.queryForObject(selectSql, Integer.class);
+ }
+
+ private Integer createMaterial(String name, String partNumber) {
+ String sql = String.format(
+ "INSERT INTO material (name, part_number, normalized_part_number, hs_code, is_deprecated) VALUES (?, ?, ?, '123456', %s)",
+ dialectProvider.getBooleanFalse());
+ executeRawSql(sql, name, partNumber, partNumber);
+
+ String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
+ return jdbcTemplate.queryForObject(selectSql, Integer.class);
+ }
+
+ private Integer createValidityPeriod(String state) {
+ String sql = String.format(
+ "INSERT INTO validity_period (state, start_date) VALUES (?, %s)",
+ isMysql() ? "NOW()" : "GETDATE()");
+ executeRawSql(sql, state);
+
+ String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
+ return jdbcTemplate.queryForObject(selectSql, Integer.class);
+ }
+
+ private Integer createPropertySet(String state) {
+ String sql = String.format(
+ "INSERT INTO property_set (state, start_date) VALUES (?, %s)",
+ isMysql() ? "NOW()" : "GETDATE()");
+ executeRawSql(sql, state);
+
+ String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
+ return jdbcTemplate.queryForObject(selectSql, Integer.class);
+ }
+
+ private Integer createPremise(Integer userId, Integer supplierNodeId, Integer materialId, Integer countryId, PremiseState state) {
+ String sql = String.format(
+ "INSERT INTO premise (user_id, supplier_node_id, material_id, country_id, state, geo_lat, geo_lng, created_at, updated_at) " +
+ "VALUES (?, ?, ?, ?, ?, 51.5, 7.5, %s, %s)",
+ isMysql() ? "NOW()" : "GETDATE()",
+ isMysql() ? "NOW()" : "GETDATE()");
+ executeRawSql(sql, userId, supplierNodeId, materialId, countryId, state.name());
+
+ String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
+ return jdbcTemplate.queryForObject(selectSql, Integer.class);
+ }
+
+ private Integer createCalculationJob(Integer premiseId, Integer validityPeriodId, Integer propertySetId, Integer userId) {
+ String sql = "INSERT INTO calculation_job (premise_id, validity_period_id, property_set_id, user_id, job_state, priority, retries) " +
+ "VALUES (?, ?, ?, ?, ?, ?, 0)";
+ executeRawSql(sql, premiseId, validityPeriodId, propertySetId, userId,
+ CalculationJobState.VALID.name(), CalculationJobPriority.MEDIUM.name());
+
+ String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
+ return jdbcTemplate.queryForObject(selectSql, Integer.class);
+ }
+
+ private Integer createDestination(Integer premiseId, Integer nodeId) {
+ String sql = "INSERT INTO premise_destination (premise_id, destination_node_id, annual_amount, country_id, geo_lat, geo_lng) " +
+ "VALUES (?, ?, 1000, ?, 51.5, 7.5)";
+ executeRawSql(sql, premiseId, nodeId, testCountryId);
+
+ String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
+ return jdbcTemplate.queryForObject(selectSql, Integer.class);
+ }
+
+ private Integer createMinimalCalculationJobDestination(Integer calculationJobId, Integer premiseId, Integer premiseDestinationId) {
+ // Simplified destination for testing route sections only
+ String sql = String.format(
+ "INSERT INTO calculation_job_destination (" +
+ "calculation_job_id, premise_destination_id, container_type, hu_count, layer_count, " +
+ "transport_weight_exceeded, is_d2d, is_small_unit, shipping_frequency, total_cost, " +
+ "annual_amount, material_cost, fca_cost, annual_risk_cost, annual_chance_cost, " +
+ "annual_repacking_cost, annual_handling_cost, annual_disposal_cost, " +
+ "operational_stock, safety_stock, stocked_inventory, in_transport_stock, stock_before_payment, " +
+ "annual_capital_cost, annual_storage_cost, custom_value, custom_duties, tariff_rate, annual_custom_cost, " +
+ "air_freight_share_max, air_freight_share, air_freight_volumetric_weight, air_freight_weight, annual_air_freight_cost, " +
+ "annual_transportation_cost, container_utilization, transit_time_in_days, safety_stock_in_days, rate_d2d" +
+ ") VALUES (" +
+ "?, ?, ?, 1, 1, " +
+ "%s, %s, %s, 1, 1000, " +
+ "1000, 50.0, 55.0, 0, 0, " +
+ "0, 0, 0, " +
+ "0, 0, 0, 0, 0, " +
+ "0, 0, 0, 0, 0, 0, " +
+ "0, 0, 0, 0, 0, " +
+ "0, 0, 15, 5, 0)",
+ dialectProvider.getBooleanFalse(),
+ dialectProvider.getBooleanFalse(),
+ dialectProvider.getBooleanFalse()
+ );
+ executeRawSql(sql, calculationJobId, premiseDestinationId, ContainerType.FEU.name());
+
+ String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
+ return jdbcTemplate.queryForObject(selectSql, Integer.class);
+ }
+
+ private Integer createRoute(Integer premiseDestinationId) {
+ String sql = String.format(
+ "INSERT INTO premise_route (premise_destination_id, is_fastest, is_cheapest, is_selected) VALUES (?, %s, %s, %s)",
+ dialectProvider.getBooleanFalse(),
+ dialectProvider.getBooleanFalse(),
+ dialectProvider.getBooleanTrue()
+ );
+ executeRawSql(sql, premiseDestinationId);
+
+ String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
+ return jdbcTemplate.queryForObject(selectSql, Integer.class);
+ }
+
+ private Integer createRouteNode(Integer nodeId, Integer countryId) {
+ String sql = String.format(
+ "INSERT INTO premise_route_node (node_id, name, external_mapping_id, country_id, is_destination, is_intermediate, is_source, is_outdated) " +
+ "VALUES (?, 'Route Node', 'RNODE-001', ?, %s, %s, %s, %s)",
+ dialectProvider.getBooleanFalse(),
+ dialectProvider.getBooleanFalse(),
+ dialectProvider.getBooleanFalse(),
+ dialectProvider.getBooleanFalse()
+ );
+ executeRawSql(sql, nodeId, countryId);
+
+ String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
+ return jdbcTemplate.queryForObject(selectSql, Integer.class);
+ }
+
+ private Integer createRouteSection(Integer routeId, Integer fromNodeId, Integer toNodeId) {
+ String sql = String.format(
+ "INSERT INTO premise_route_section (premise_route_id, from_route_node_id, to_route_node_id, list_position, " +
+ "transport_type, rate_type, is_pre_run, is_main_run, is_post_run, is_outdated) " +
+ "VALUES (?, ?, ?, 1, ?, ?, %s, %s, %s, %s)",
+ dialectProvider.getBooleanFalse(),
+ dialectProvider.getBooleanTrue(),
+ dialectProvider.getBooleanFalse(),
+ dialectProvider.getBooleanFalse()
+ );
+ executeRawSql(sql, routeId, fromNodeId, toNodeId, TransportType.SEA.name(), RateType.CONTAINER.name());
+
+ String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
+ return jdbcTemplate.queryForObject(selectSql, Integer.class);
+ }
+}