diff --git a/src/main/java/de/avatic/lcc/repositories/premise/DestinationRepository.java b/src/main/java/de/avatic/lcc/repositories/premise/DestinationRepository.java
index 70dde7f..f44db8d 100644
--- a/src/main/java/de/avatic/lcc/repositories/premise/DestinationRepository.java
+++ b/src/main/java/de/avatic/lcc/repositories/premise/DestinationRepository.java
@@ -9,6 +9,7 @@ import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
+import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -19,7 +20,7 @@ import java.sql.Statement;
import java.util.*;
import java.util.stream.Collectors;
-@Service
+@Repository
public class DestinationRepository {
private final JdbcTemplate jdbcTemplate;
diff --git a/src/test/java/de/avatic/lcc/config/RepositoryTestConfig.java b/src/test/java/de/avatic/lcc/config/RepositoryTestConfig.java
index d228534..4dec697 100644
--- a/src/test/java/de/avatic/lcc/config/RepositoryTestConfig.java
+++ b/src/test/java/de/avatic/lcc/config/RepositoryTestConfig.java
@@ -28,8 +28,7 @@ import javax.sql.DataSource;
excludeFilters = @ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = {
- de.avatic.lcc.repositories.error.DumpRepository.class,
- de.avatic.lcc.repositories.premise.DestinationRepository.class
+ de.avatic.lcc.repositories.error.DumpRepository.class
}
)
)
diff --git a/src/test/java/de/avatic/lcc/repositories/premise/DestinationRepositoryIntegrationTest.java b/src/test/java/de/avatic/lcc/repositories/premise/DestinationRepositoryIntegrationTest.java
new file mode 100644
index 0000000..f2c9c3d
--- /dev/null
+++ b/src/test/java/de/avatic/lcc/repositories/premise/DestinationRepositoryIntegrationTest.java
@@ -0,0 +1,527 @@
+package de.avatic.lcc.repositories.premise;
+
+import de.avatic.lcc.model.db.premises.PremiseState;
+import de.avatic.lcc.model.db.premises.route.Destination;
+import de.avatic.lcc.repositories.AbstractRepositoryIntegrationTest;
+import de.avatic.lcc.util.exception.base.ForbiddenException;
+import de.avatic.lcc.util.exception.internalerror.DatabaseException;
+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.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Integration tests for DestinationRepository.
+ *
+ * Tests critical functionality across both MySQL and MSSQL:
+ * - Dynamic IN clauses
+ * - Named parameters
+ * - JOIN queries
+ * - NULL handling
+ * - Authorization checks
+ * - BigDecimal field operations
+ *
+ * Run with:
+ *
+ * mvn test -Dspring.profiles.active=test,mysql -Dtest=DestinationRepositoryIntegrationTest
+ * mvn test -Dspring.profiles.active=test,mssql -Dtest=DestinationRepositoryIntegrationTest
+ *
+ */
+class DestinationRepositoryIntegrationTest extends AbstractRepositoryIntegrationTest {
+
+ @Autowired
+ private DestinationRepository destinationRepository;
+
+ private Integer testUserId1;
+ private Integer testUserId2;
+ private Integer testCountryId;
+ private Integer testNodeId1;
+ private Integer testNodeId2;
+ private Integer testMaterialId;
+ private Integer testPremiseId1;
+ private Integer testPremiseId2;
+
+ @BeforeEach
+ void setupTestData() {
+ // Clean up in correct order (respecting foreign key constraints)
+ jdbcTemplate.update("DELETE FROM premise_route_section");
+ 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 before deleting nodes
+ 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");
+
+ // Create test users
+ testUserId1 = createUser("WD001", "user1@example.com");
+ testUserId2 = createUser("WD002", "user2@example.com");
+
+ // Get test country
+ testCountryId = getCountryId("DE");
+
+ // Create test nodes
+ testNodeId1 = createNode("Node 1", "NODE-001", testCountryId);
+ testNodeId2 = createNode("Node 2", "NODE-002", testCountryId);
+
+ // Create test material
+ testMaterialId = createMaterial("Test Material", "MAT-001");
+
+ // Create test premises
+ testPremiseId1 = createPremise(testUserId1, testNodeId1, testMaterialId, testCountryId, PremiseState.DRAFT);
+ testPremiseId2 = createPremise(testUserId2, testNodeId1, testMaterialId, testCountryId, PremiseState.DRAFT);
+
+ // Create some test destinations
+ createDestination(testPremiseId1, testNodeId1, 1000);
+ createDestination(testPremiseId1, testNodeId2, 2000);
+ createDestination(testPremiseId2, testNodeId1, 3000);
+ }
+
+ @Test
+ void testGetById() {
+ // Given: Create destination
+ Integer destinationId = createDestination(testPremiseId1, testNodeId1, 500);
+
+ // When: Get by ID
+ Optional destination = destinationRepository.getById(destinationId);
+
+ // Then: Should retrieve
+ assertTrue(destination.isPresent());
+ assertEquals(destinationId, destination.get().getId());
+ assertEquals(testPremiseId1, destination.get().getPremiseId());
+ assertEquals(testNodeId1, destination.get().getDestinationNodeId());
+ assertEquals(500, destination.get().getAnnualAmount());
+ }
+
+ @Test
+ void testGetByIdNotFound() {
+ // When: Get non-existent ID
+ Optional destination = destinationRepository.getById(99999);
+
+ // Then: Should return empty
+ assertFalse(destination.isPresent());
+ }
+
+ @Test
+ void testGetByPremiseId() {
+ // When: Get destinations for premise1
+ List destinations = destinationRepository.getByPremiseId(testPremiseId1);
+
+ // Then: Should return all destinations for premise1
+ assertNotNull(destinations);
+ assertEquals(2, destinations.size());
+ assertTrue(destinations.stream().allMatch(d -> d.getPremiseId().equals(testPremiseId1)));
+ }
+
+ @Test
+ void testGetByPremiseIdAndUserId() {
+ // When: Get destinations for premise1 with correct user
+ List destinations = destinationRepository.getByPremiseIdAndUserId(testPremiseId1, testUserId1);
+
+ // Then: Should return destinations
+ assertNotNull(destinations);
+ assertEquals(2, destinations.size());
+ }
+
+ @Test
+ void testGetByPremiseIdAndUserIdWrongUser() {
+ // When: Get destinations for premise1 with wrong user
+ List destinations = destinationRepository.getByPremiseIdAndUserId(testPremiseId1, testUserId2);
+
+ // Then: Should return empty
+ assertTrue(destinations.isEmpty());
+ }
+
+ @Test
+ void testGetByPremiseIdAndUserIdNonExistent() {
+ // When: Get destinations for non-existent premise
+ List destinations = destinationRepository.getByPremiseIdAndUserId(99999, testUserId1);
+
+ // Then: Should return empty
+ assertTrue(destinations.isEmpty());
+ }
+
+ @Test
+ void testUpdate() {
+ // Given: Create destination
+ Integer destinationId = createDestination(testPremiseId1, testNodeId1, 500);
+
+ // When: Update
+ destinationRepository.update(
+ destinationId,
+ 1500, // annualAmount
+ new BigDecimal("10.50"), // repackingCost
+ new BigDecimal("5.25"), // disposalCost
+ new BigDecimal("8.75"), // handlingCost
+ true, // isD2d
+ new BigDecimal("100.00"), // d2dRate
+ new BigDecimal("48.00"), // d2dLeadTime
+ new BigDecimal("150.5") // distanceD2d
+ );
+
+ // Then: Should be updated
+ Optional updated = destinationRepository.getById(destinationId);
+ assertTrue(updated.isPresent());
+ assertEquals(1500, updated.get().getAnnualAmount());
+ assertEquals(0, new BigDecimal("10.50").compareTo(updated.get().getRepackingCost()));
+ assertEquals(0, new BigDecimal("5.25").compareTo(updated.get().getDisposalCost()));
+ assertEquals(0, new BigDecimal("8.75").compareTo(updated.get().getHandlingCost()));
+ assertTrue(updated.get().getD2d());
+ assertEquals(0, new BigDecimal("100.00").compareTo(updated.get().getRateD2d()));
+ }
+
+ @Test
+ void testUpdateWithNulls() {
+ // Given: Create destination
+ Integer destinationId = createDestination(testPremiseId1, testNodeId1, 500);
+
+ // When: Update with null values
+ destinationRepository.update(
+ destinationId,
+ null, // annualAmount
+ null, // repackingCost
+ null, // disposalCost
+ null, // handlingCost
+ false, // isD2d
+ null, // d2dRate (should be null when isD2d is false)
+ null, // d2dLeadTime
+ null // distanceD2d
+ );
+
+ // Then: Should be updated with nulls
+ Optional updated = destinationRepository.getById(destinationId);
+ assertTrue(updated.isPresent());
+ assertFalse(updated.get().getD2d());
+ assertNull(updated.get().getRateD2d());
+ }
+
+ @Test
+ void testUpdateNonExistent() {
+ // When/Then: Update non-existent destination should throw
+ assertThrows(DatabaseException.class, () ->
+ destinationRepository.update(99999, 100, null, null, null, false, null, null, null));
+ }
+
+ @Test
+ void testDeleteById() {
+ // Given: Create destination
+ Integer destinationId = createDestination(testPremiseId1, testNodeId1, 500);
+
+ // When: Delete
+ destinationRepository.deleteById(destinationId);
+
+ // Then: Should be deleted
+ Optional deleted = destinationRepository.getById(destinationId);
+ assertFalse(deleted.isPresent());
+ }
+
+ @Test
+ void testDeleteByIdNull() {
+ // When: Delete with null (should not throw)
+ destinationRepository.deleteById(null);
+
+ // Then: No error
+ assertTrue(true);
+ }
+
+ @Test
+ void testGetOwnerIdById() {
+ // Given: Create destination for user1
+ Integer destinationId = createDestination(testPremiseId1, testNodeId1, 500);
+
+ // When: Get owner ID
+ Optional ownerId = destinationRepository.getOwnerIdById(destinationId);
+
+ // Then: Should return user1's ID
+ assertTrue(ownerId.isPresent());
+ assertEquals(testUserId1, ownerId.get());
+ }
+
+ @Test
+ void testGetOwnerIdByIdNotFound() {
+ // When: Get owner ID for non-existent destination
+ Optional ownerId = destinationRepository.getOwnerIdById(99999);
+
+ // Then: Should return empty
+ assertFalse(ownerId.isPresent());
+ }
+
+ @Test
+ void testGetOwnerIdsByIds() {
+ // Given: Create destinations for different users
+ Integer dest1 = createDestination(testPremiseId1, testNodeId1, 500);
+ Integer dest2 = createDestination(testPremiseId1, testNodeId2, 600);
+ Integer dest3 = createDestination(testPremiseId2, testNodeId1, 700);
+
+ // When: Get owner IDs
+ Map ownerMap = destinationRepository.getOwnerIdsByIds(List.of(dest1, dest2, dest3));
+
+ // Then: Should return correct mappings
+ assertEquals(3, ownerMap.size());
+ assertEquals(testUserId1, ownerMap.get(dest1));
+ assertEquals(testUserId1, ownerMap.get(dest2));
+ assertEquals(testUserId2, ownerMap.get(dest3));
+ }
+
+ @Test
+ void testGetOwnerIdsByIdsEmpty() {
+ // When: Get owner IDs for empty list
+ Map ownerMap = destinationRepository.getOwnerIdsByIds(List.of());
+
+ // Then: Should return empty map
+ assertTrue(ownerMap.isEmpty());
+ }
+
+ @Test
+ void testGetOwnerIdsByIdsNull() {
+ // When: Get owner IDs for null
+ Map ownerMap = destinationRepository.getOwnerIdsByIds(null);
+
+ // Then: Should return empty map
+ assertTrue(ownerMap.isEmpty());
+ }
+
+ @Test
+ void testGetByPremiseIdsAndNodeIdsMap() {
+ // Given: Map of premise IDs to node IDs
+ Map> premiseToNodes = new HashMap<>();
+ premiseToNodes.put(testPremiseId1, List.of(testNodeId1, testNodeId2));
+ premiseToNodes.put(testPremiseId2, List.of(testNodeId1));
+
+ // When: Get destinations
+ Map> result =
+ destinationRepository.getByPremiseIdsAndNodeIds(premiseToNodes, testUserId1);
+
+ // Then: Should return destinations for user1's premises only
+ assertNotNull(result);
+ assertTrue(result.containsKey(testPremiseId1));
+ assertEquals(2, result.get(testPremiseId1).size());
+ }
+
+ @Test
+ void testGetByPremiseIdsAndNodeIdsMapEmpty() {
+ // When: Get with empty map
+ Map> result =
+ destinationRepository.getByPremiseIdsAndNodeIds(Map.of(), testUserId1);
+
+ // Then: Should return empty map
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ void testGetByPremiseIdsAndNodeIdsLists() {
+ // When: Get destinations by lists
+ Map> result = destinationRepository.getByPremiseIdsAndNodeIds(
+ List.of(testPremiseId1, testPremiseId2),
+ List.of(testNodeId1, testNodeId2),
+ testUserId1
+ );
+
+ // Then: Should return only user1's destinations
+ // Note: The method queries for all premises in the list, then filters by userId in the JOIN
+ // So premise2 (owned by user2) should NOT be returned
+ assertNotNull(result);
+ assertTrue(result.containsKey(testPremiseId1));
+
+ // The method returns empty list for premises not owned by the user
+ if (result.containsKey(testPremiseId2)) {
+ assertTrue(result.get(testPremiseId2).isEmpty(), "User2's premise should return empty list");
+ }
+ }
+
+ @Test
+ void testInsert() {
+ // Given: New destination
+ Destination newDest = new Destination();
+ newDest.setPremiseId(testPremiseId1);
+ newDest.setDestinationNodeId(testNodeId1);
+ newDest.setCountryId(testCountryId);
+ newDest.setAnnualAmount(999);
+ newDest.setRepackingCost(new BigDecimal("12.50"));
+ newDest.setHandlingCost(new BigDecimal("8.25"));
+ newDest.setDisposalCost(new BigDecimal("3.75"));
+ newDest.setD2d(true);
+ newDest.setRateD2d(new BigDecimal("50.00"));
+ newDest.setLeadTimeD2d(24);
+ newDest.setGeoLat(new BigDecimal("51.5"));
+ newDest.setGeoLng(new BigDecimal("7.5"));
+ newDest.setDistanceD2d(new BigDecimal("100.0"));
+
+ // When: Insert
+ Integer id = destinationRepository.insert(newDest);
+
+ // Then: Should be inserted
+ assertNotNull(id);
+ assertTrue(id > 0);
+
+ Optional inserted = destinationRepository.getById(id);
+ assertTrue(inserted.isPresent());
+ assertEquals(999, inserted.get().getAnnualAmount());
+ assertEquals(0, new BigDecimal("12.50").compareTo(inserted.get().getRepackingCost()));
+ assertTrue(inserted.get().getD2d());
+ }
+
+ @Test
+ void testInsertWithNulls() {
+ // Given: New destination with nullable fields as null
+ Destination newDest = new Destination();
+ newDest.setPremiseId(testPremiseId1);
+ newDest.setDestinationNodeId(testNodeId1);
+ newDest.setCountryId(testCountryId);
+ newDest.setAnnualAmount(null); // nullable
+ newDest.setRepackingCost(null);
+ newDest.setHandlingCost(null);
+ newDest.setDisposalCost(null);
+ newDest.setD2d(false);
+ newDest.setRateD2d(null);
+ newDest.setLeadTimeD2d(null); // nullable
+ newDest.setGeoLat(new BigDecimal("51.5"));
+ newDest.setGeoLng(new BigDecimal("7.5"));
+ newDest.setDistanceD2d(null);
+
+ // When: Insert
+ Integer id = destinationRepository.insert(newDest);
+
+ // Then: Should be inserted successfully
+ assertNotNull(id);
+
+ Optional inserted = destinationRepository.getById(id);
+ assertTrue(inserted.isPresent());
+ // Note: annualAmount might be null or 0 depending on database behavior with nullable INT
+ // Just verify the record was inserted with the non-null fields
+ assertEquals(testPremiseId1, inserted.get().getPremiseId());
+ assertEquals(testNodeId1, inserted.get().getDestinationNodeId());
+ assertFalse(inserted.get().getD2d());
+ }
+
+ @Test
+ void testCheckOwnerSuccess() {
+ // Given: Destination owned by user1
+ Integer destId = createDestination(testPremiseId1, testNodeId1, 500);
+
+ // When/Then: Check with correct owner should not throw
+ assertDoesNotThrow(() -> destinationRepository.checkOwner(destId, testUserId1));
+ }
+
+ @Test
+ void testCheckOwnerWrongUser() {
+ // Given: Destination owned by user1
+ Integer destId = createDestination(testPremiseId1, testNodeId1, 500);
+
+ // When/Then: Check with wrong user should throw
+ assertThrows(ForbiddenException.class, () ->
+ destinationRepository.checkOwner(destId, testUserId2));
+ }
+
+ @Test
+ void testCheckOwnerNonExistent() {
+ // When/Then: Check non-existent destination should throw
+ assertThrows(ForbiddenException.class, () ->
+ destinationRepository.checkOwner(99999, testUserId1));
+ }
+
+ @Test
+ void testCheckOwnerListSuccess() {
+ // Given: Destinations owned by user1
+ Integer dest1 = createDestination(testPremiseId1, testNodeId1, 500);
+ Integer dest2 = createDestination(testPremiseId1, testNodeId2, 600);
+
+ // When/Then: Check with correct owner should not throw
+ assertDoesNotThrow(() -> destinationRepository.checkOwner(List.of(dest1, dest2), testUserId1));
+ }
+
+ @Test
+ void testCheckOwnerListMixedOwners() {
+ // Given: Destinations with different owners
+ Integer dest1 = createDestination(testPremiseId1, testNodeId1, 500);
+ Integer dest2 = createDestination(testPremiseId2, testNodeId1, 600);
+
+ // When/Then: Check with user1 should throw (dest2 is owned by user2)
+ assertThrows(ForbiddenException.class, () ->
+ destinationRepository.checkOwner(List.of(dest1, dest2), testUserId1));
+ }
+
+ @Test
+ void testCheckOwnerListEmpty() {
+ // When/Then: Check with empty list should not throw
+ assertDoesNotThrow(() -> destinationRepository.checkOwner(List.of(), testUserId1));
+ }
+
+ @Test
+ void testCheckOwnerListNull() {
+ // When/Then: Check with null should not throw
+ assertDoesNotThrow(() -> destinationRepository.checkOwner((List) null, testUserId1));
+ }
+
+ // ========== Helper Methods ==========
+
+ 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 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, Integer annualAmount) {
+ String sql = "INSERT INTO premise_destination (premise_id, destination_node_id, annual_amount, country_id, geo_lat, geo_lng) " +
+ "VALUES (?, ?, ?, ?, 51.5, 7.5)";
+ executeRawSql(sql, premiseId, nodeId, annualAmount, testCountryId);
+
+ 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/premise/PremiseRepositoryIntegrationTest.java b/src/test/java/de/avatic/lcc/repositories/premise/PremiseRepositoryIntegrationTest.java
new file mode 100644
index 0000000..b16b0dc
--- /dev/null
+++ b/src/test/java/de/avatic/lcc/repositories/premise/PremiseRepositoryIntegrationTest.java
@@ -0,0 +1,440 @@
+package de.avatic.lcc.repositories.premise;
+
+import de.avatic.lcc.model.db.premises.Premise;
+import de.avatic.lcc.model.db.premises.PremiseListEntry;
+import de.avatic.lcc.model.db.premises.PremiseState;
+import de.avatic.lcc.repositories.AbstractRepositoryIntegrationTest;
+import de.avatic.lcc.repositories.pagination.SearchQueryPagination;
+import de.avatic.lcc.repositories.pagination.SearchQueryResult;
+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.Optional;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Integration tests for PremiseRepository.
+ *
+ * Tests critical functionality across both MySQL and MSSQL:
+ * - Pagination (LIMIT/OFFSET vs OFFSET/FETCH)
+ * - CURRENT_TIMESTAMP functions (NOW() vs GETDATE())
+ * - Boolean literals (TRUE/FALSE vs 1/0)
+ * - Dynamic IN clauses
+ * - Complex JOIN queries with filtering
+ *
+ * Run with:
+ *
+ * mvn test -Dspring.profiles.active=test,mysql -Dtest=PremiseRepositoryIntegrationTest
+ * mvn test -Dspring.profiles.active=test,mssql -Dtest=PremiseRepositoryIntegrationTest
+ *
+ */
+class PremiseRepositoryIntegrationTest extends AbstractRepositoryIntegrationTest {
+
+ @Autowired
+ private PremiseRepository premiseRepository;
+
+ private Integer testUserId;
+ private Integer testCountryId;
+ private Integer testNodeId;
+ private Integer testMaterialId;
+
+ @BeforeEach
+ void setupTestData() {
+ // Clean up in correct order (respecting foreign key constraints)
+ jdbcTemplate.update("DELETE FROM premise_destination");
+ jdbcTemplate.update("DELETE FROM premise");
+ jdbcTemplate.update("DELETE FROM material");
+
+ // Clean up node-referencing tables before deleting nodes
+ 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");
+
+ // Create test user
+ testUserId = createUser("WD001", "test@example.com");
+
+ // Get test country
+ testCountryId = getCountryId("DE");
+
+ // Create test node
+ testNodeId = createNode("Test Supplier", "SUP-001", testCountryId);
+
+ // Create test material
+ testMaterialId = createMaterial("Test Material", "MAT-001");
+
+ // Create some test premises
+ createPremise(testUserId, testNodeId, testMaterialId, testCountryId, PremiseState.DRAFT);
+ createPremise(testUserId, testNodeId, testMaterialId, testCountryId, PremiseState.COMPLETED);
+ createPremise(testUserId, testNodeId, testMaterialId, testCountryId, PremiseState.ARCHIVED);
+ }
+
+ @Test
+ void testListPremises() {
+ // Given: Pagination
+ SearchQueryPagination pagination = new SearchQueryPagination(1, 10);
+
+ // When: List premises
+ SearchQueryResult result = premiseRepository.listPremises(
+ null, pagination, testUserId, null, null, null);
+
+ // Then: Should return all premises
+ assertNotNull(result);
+ assertEquals(3, result.getTotalElements());
+ assertFalse(result.toList().isEmpty());
+ }
+
+ @Test
+ void testListPremisesPagination() {
+ // Given: Create more premises and use pagination
+ for (int i = 0; i < 10; i++) {
+ createPremise(testUserId, testNodeId, testMaterialId, testCountryId, PremiseState.DRAFT);
+ }
+
+ SearchQueryPagination pagination = new SearchQueryPagination(1, 5);
+
+ // When: List premises
+ SearchQueryResult result = premiseRepository.listPremises(
+ null, pagination, testUserId, null, null, null);
+
+ // Then: Should respect limit
+ assertNotNull(result);
+ assertEquals(5, result.toList().size());
+ assertTrue(result.getTotalElements() >= 13);
+ }
+
+ @Test
+ void testListPremisesWithFilter() {
+ // Given: Pagination and filter
+ SearchQueryPagination pagination = new SearchQueryPagination(1, 10);
+
+ // When: List with filter
+ SearchQueryResult result = premiseRepository.listPremises(
+ "Test Supplier", pagination, testUserId, null, null, null);
+
+ // Then: Should filter results
+ assertNotNull(result);
+ assertTrue(result.getTotalElements() >= 3);
+ assertTrue(result.toList().stream()
+ .allMatch(p -> p.getSupplierName().contains("Test Supplier")));
+ }
+
+ @Test
+ void testListPremisesWithDraftFilter() {
+ // Given: Pagination with draft filter
+ SearchQueryPagination pagination = new SearchQueryPagination(1, 10);
+
+ // When: List only drafts
+ SearchQueryResult result = premiseRepository.listPremises(
+ null, pagination, testUserId, true, false, false);
+
+ // Then: Should return only DRAFT premises
+ assertNotNull(result);
+ assertTrue(result.toList().stream()
+ .allMatch(p -> p.getState() == de.avatic.lcc.dto.calculation.PremiseState.DRAFT));
+ }
+
+ @Test
+ void testInsert() {
+ // When: Insert new premise
+ Integer premiseId = premiseRepository.insert(
+ testMaterialId, testNodeId, null,
+ new BigDecimal("51.5"), new BigDecimal("7.5"),
+ testCountryId, testUserId);
+
+ // Then: Should be inserted
+ assertNotNull(premiseId);
+ assertTrue(premiseId > 0);
+
+ Optional inserted = premiseRepository.getPremiseById(premiseId);
+ assertTrue(inserted.isPresent());
+ assertEquals(testMaterialId, inserted.get().getMaterialId());
+ assertEquals(testNodeId, inserted.get().getSupplierNodeId());
+ assertEquals(PremiseState.DRAFT, inserted.get().getState());
+ }
+
+ @Test
+ void testGetPremiseById() {
+ // Given: Create premise
+ Integer premiseId = createPremise(testUserId, testNodeId, testMaterialId, testCountryId, PremiseState.DRAFT);
+
+ // When: Get by ID
+ Optional premise = premiseRepository.getPremiseById(premiseId);
+
+ // Then: Should retrieve
+ assertTrue(premise.isPresent());
+ assertEquals(premiseId, premise.get().getId());
+ assertEquals(testMaterialId, premise.get().getMaterialId());
+ }
+
+ @Test
+ void testGetPremiseByIdNotFound() {
+ // When: Get non-existent ID
+ Optional premise = premiseRepository.getPremiseById(99999);
+
+ // Then: Should return empty
+ assertFalse(premise.isPresent());
+ }
+
+ @Test
+ void testGetPremisesById() {
+ // Given: Create multiple premises
+ Integer id1 = createPremise(testUserId, testNodeId, testMaterialId, testCountryId, PremiseState.DRAFT);
+ Integer id2 = createPremise(testUserId, testNodeId, testMaterialId, testCountryId, PremiseState.DRAFT);
+
+ // When: Get by IDs
+ List premises = premiseRepository.getPremisesById(List.of(id1, id2));
+
+ // Then: Should retrieve both
+ assertNotNull(premises);
+ assertEquals(2, premises.size());
+ assertTrue(premises.stream().anyMatch(p -> p.getId().equals(id1)));
+ assertTrue(premises.stream().anyMatch(p -> p.getId().equals(id2)));
+ }
+
+ @Test
+ void testGetPremisesByIdEmpty() {
+ // When: Get with empty list
+ List premises = premiseRepository.getPremisesById(List.of());
+
+ // Then: Should return empty
+ assertNotNull(premises);
+ assertTrue(premises.isEmpty());
+ }
+
+ @Test
+ void testResetPrice() {
+ // Given: Create premise with price
+ Integer premiseId = createPremise(testUserId, testNodeId, testMaterialId, testCountryId, PremiseState.DRAFT);
+
+ // Set initial price
+ premiseRepository.updatePrice(List.of(premiseId), new BigDecimal("100.50"), true, new BigDecimal("0.5"));
+
+ // When: Reset price
+ premiseRepository.resetPrice(List.of(premiseId));
+
+ // Then: Price should be null
+ Optional premise = premiseRepository.getPremiseById(premiseId);
+ assertTrue(premise.isPresent());
+ assertNull(premise.get().getMaterialCost());
+ assertFalse(premise.get().getFcaEnabled());
+ }
+
+ @Test
+ void testUpdatePrice() {
+ // Given: Create premise
+ Integer premiseId = createPremise(testUserId, testNodeId, testMaterialId, testCountryId, PremiseState.DRAFT);
+
+ // When: Update price
+ premiseRepository.updatePrice(List.of(premiseId), new BigDecimal("200.75"), true, new BigDecimal("0.6"));
+
+ // Then: Price should be updated
+ Optional premise = premiseRepository.getPremiseById(premiseId);
+ assertTrue(premise.isPresent());
+ assertEquals(0, new BigDecimal("200.75").compareTo(premise.get().getMaterialCost()));
+ assertTrue(premise.get().getFcaEnabled());
+ assertEquals(0, new BigDecimal("0.6").compareTo(premise.get().getOverseaShare()));
+ }
+
+ @Test
+ void testUpdateMaterial() {
+ // Given: Create premise
+ Integer premiseId = createPremise(testUserId, testNodeId, testMaterialId, testCountryId, PremiseState.DRAFT);
+
+ // When: Update material properties
+ premiseRepository.updateMaterial(List.of(premiseId), "12345678", new BigDecimal("0.05"), true);
+
+ // Then: Material properties should be updated
+ Optional premise = premiseRepository.getPremiseById(premiseId);
+ assertTrue(premise.isPresent());
+ assertEquals("12345678", premise.get().getHsCode());
+ assertEquals(0, new BigDecimal("0.05").compareTo(premise.get().getTariffRate()));
+ assertTrue(premise.get().getTariffUnlocked());
+ }
+
+ @Test
+ void testSetMaterialId() {
+ // Given: Create premise and new material
+ Integer premiseId = createPremise(testUserId, testNodeId, testMaterialId, testCountryId, PremiseState.DRAFT);
+ Integer newMaterialId = createMaterial("New Material", "MAT-002");
+
+ // When: Set new material ID
+ premiseRepository.setMaterialId(List.of(premiseId), newMaterialId);
+
+ // Then: Material should be changed
+ Optional premise = premiseRepository.getPremiseById(premiseId);
+ assertTrue(premise.isPresent());
+ assertEquals(newMaterialId, premise.get().getMaterialId());
+ }
+
+ @Test
+ void testDeletePremisesById() {
+ // Given: Create DRAFT premises
+ Integer draftId = createPremise(testUserId, testNodeId, testMaterialId, testCountryId, PremiseState.DRAFT);
+ Integer completedId = createPremise(testUserId, testNodeId, testMaterialId, testCountryId, PremiseState.COMPLETED);
+
+ // When: Delete (should only delete DRAFT)
+ premiseRepository.deletePremisesById(List.of(draftId, completedId));
+
+ // Then: DRAFT should be deleted, COMPLETED should remain
+ assertFalse(premiseRepository.getPremiseById(draftId).isPresent());
+ assertTrue(premiseRepository.getPremiseById(completedId).isPresent());
+ }
+
+ @Test
+ void testSetStatus() {
+ // Given: Create DRAFT premise
+ Integer premiseId = createPremise(testUserId, testNodeId, testMaterialId, testCountryId, PremiseState.DRAFT);
+
+ // When: Set status to COMPLETED
+ premiseRepository.setStatus(List.of(premiseId), PremiseState.COMPLETED);
+
+ // Then: Status should be updated
+ Optional premise = premiseRepository.getPremiseById(premiseId);
+ assertTrue(premise.isPresent());
+ assertEquals(PremiseState.COMPLETED, premise.get().getState());
+ }
+
+ @Test
+ void testFindByMaterialIdAndSupplierId() {
+ // Given: Create premise
+ createPremise(testUserId, testNodeId, testMaterialId, testCountryId, PremiseState.DRAFT);
+
+ // When: Find by material and supplier
+ List premises = premiseRepository.findByMaterialIdAndSupplierId(
+ testMaterialId, testNodeId, null, testUserId);
+
+ // Then: Should find premise
+ assertNotNull(premises);
+ assertFalse(premises.isEmpty());
+ assertTrue(premises.stream().anyMatch(p ->
+ p.getMaterialId().equals(testMaterialId) && p.getSupplierNodeId().equals(testNodeId)));
+ }
+
+ @Test
+ void testGetPremisesByMaterialIdsAndSupplierIds() {
+ // Given: Create premises
+ createPremise(testUserId, testNodeId, testMaterialId, testCountryId, PremiseState.DRAFT);
+
+ // When: Get by IDs
+ List premises = premiseRepository.getPremisesByMaterialIdsAndSupplierIds(
+ List.of(testMaterialId), List.of(testNodeId), null, testUserId, true);
+
+ // Then: Should find premises
+ assertNotNull(premises);
+ assertFalse(premises.isEmpty());
+ assertTrue(premises.stream().allMatch(p -> p.getState() == PremiseState.DRAFT));
+ }
+
+ @Test
+ void testFindAssociatedSuppliers() {
+ // Given: Premises with suppliers
+ createPremise(testUserId, testNodeId, testMaterialId, testCountryId, PremiseState.DRAFT);
+
+ // When: Find associated suppliers
+ List supplierIds = premiseRepository.findAssociatedSuppliers(List.of(testMaterialId));
+
+ // Then: Should find supplier
+ assertNotNull(supplierIds);
+ assertFalse(supplierIds.isEmpty());
+ assertTrue(supplierIds.contains(testNodeId));
+ }
+
+ @Test
+ void testGetIdsWithUnlockedTariffs() {
+ // Given: Create premises with different tariff states
+ Integer unlockedId = createPremise(testUserId, testNodeId, testMaterialId, testCountryId, PremiseState.DRAFT);
+ Integer lockedId = createPremise(testUserId, testNodeId, testMaterialId, testCountryId, PremiseState.DRAFT);
+
+ // Set one as unlocked
+ premiseRepository.updateMaterial(List.of(unlockedId), null, null, true);
+
+ // When: Get unlocked IDs
+ List unlockedIds = premiseRepository.getIdsWithUnlockedTariffs(List.of(unlockedId, lockedId));
+
+ // Then: Should return only unlocked
+ assertNotNull(unlockedIds);
+ assertTrue(unlockedIds.contains(unlockedId));
+ assertFalse(unlockedIds.contains(lockedId));
+ }
+
+ @Test
+ void testGetPremiseCompletedCountByUserId() {
+ // When: Get count
+ Integer count = premiseRepository.getPremiseCompletedCountByUserId(testUserId);
+
+ // Then: Should count COMPLETED premises
+ assertNotNull(count);
+ assertTrue(count >= 1, "Should have at least 1 COMPLETED premise from setup");
+ }
+
+ @Test
+ void testGetPremiseDraftCountByUserId() {
+ // When: Get count
+ Integer count = premiseRepository.getPremiseDraftCountByUserId(testUserId);
+
+ // Then: Should count DRAFT premises
+ assertNotNull(count);
+ assertTrue(count >= 1, "Should have at least 1 DRAFT premise from setup");
+ }
+
+ // ========== Helper Methods ==========
+
+ 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 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);
+ }
+}
diff --git a/src/test/java/de/avatic/lcc/repositories/premise/RouteNodeRepositoryIntegrationTest.java b/src/test/java/de/avatic/lcc/repositories/premise/RouteNodeRepositoryIntegrationTest.java
new file mode 100644
index 0000000..2f57ab1
--- /dev/null
+++ b/src/test/java/de/avatic/lcc/repositories/premise/RouteNodeRepositoryIntegrationTest.java
@@ -0,0 +1,444 @@
+package de.avatic.lcc.repositories.premise;
+
+import de.avatic.lcc.model.db.premises.PremiseState;
+import de.avatic.lcc.model.db.premises.route.RouteNode;
+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.Optional;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Integration tests for RouteNodeRepository.
+ *
+ * Tests critical functionality across both MySQL and MSSQL:
+ * - Boolean literals (TRUE/FALSE vs 1/0)
+ * - Dynamic IN clauses
+ * - JOIN queries
+ * - NULL handling for optional foreign keys
+ * - BigDecimal geo coordinates
+ *
+ * Run with:
+ *
+ * mvn test -Dspring.profiles.active=test,mysql -Dtest=RouteNodeRepositoryIntegrationTest
+ * mvn test -Dspring.profiles.active=test,mssql -Dtest=RouteNodeRepositoryIntegrationTest
+ *
+ */
+class RouteNodeRepositoryIntegrationTest extends AbstractRepositoryIntegrationTest {
+
+ @Autowired
+ private RouteNodeRepository routeNodeRepository;
+
+ private Integer testUserId;
+ private Integer testCountryId;
+ private Integer testNodeId;
+ private Integer testMaterialId;
+ private Integer testPremiseId;
+ private Integer testDestinationId;
+ private Integer testRouteId;
+
+ @BeforeEach
+ void setupTestData() {
+ // Clean up in correct order (respecting foreign key constraints)
+ 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 before deleting nodes
+ 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");
+
+ // 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 premise
+ testPremiseId = createPremise(testUserId, testNodeId, testMaterialId, testCountryId, PremiseState.DRAFT);
+
+ // Create test destination
+ testDestinationId = createDestination(testPremiseId, testNodeId);
+
+ // Create test route
+ testRouteId = createRoute(testDestinationId);
+
+ // Create some test route nodes
+ createRouteNode("Node A", testNodeId, testCountryId, true, false, false);
+ createRouteNode("Node B", testNodeId, testCountryId, false, true, false);
+ createRouteNode("Node C", testNodeId, testCountryId, false, false, true);
+ }
+
+ @Test
+ void testGetById() {
+ // Given: Create route node
+ Integer nodeId = createRouteNode("Test Node", testNodeId, testCountryId, true, true, true);
+
+ // When: Get by ID
+ Optional node = routeNodeRepository.getById(nodeId);
+
+ // Then: Should retrieve
+ assertTrue(node.isPresent());
+ assertEquals(nodeId, node.get().getId());
+ assertEquals("Test Node", node.get().getName());
+ assertTrue(node.get().getDestination());
+ assertTrue(node.get().getIntermediate());
+ assertTrue(node.get().getSource());
+ }
+
+ @Test
+ void testGetByIdNotFound() {
+ // When: Get non-existent ID
+ Optional node = routeNodeRepository.getById(99999);
+
+ // Then: Should return empty
+ assertFalse(node.isPresent());
+ }
+
+ @Test
+ void testInsert() {
+ // Given: New route node
+ RouteNode newNode = new RouteNode();
+ newNode.setName("New Route Node");
+ newNode.setAddress("123 Test Street");
+ newNode.setGeoLat(new BigDecimal("51.5074"));
+ newNode.setGeoLng(new BigDecimal("0.1278"));
+ newNode.setDestination(true);
+ newNode.setIntermediate(false);
+ newNode.setSource(true);
+ newNode.setNodeId(testNodeId);
+ newNode.setUserNodeId(null);
+ newNode.setOutdated(false);
+ newNode.setCountryId(testCountryId);
+ newNode.setExternalMappingId("EXT-001");
+
+ // When: Insert
+ Integer id = routeNodeRepository.insert(newNode);
+
+ // Then: Should be inserted
+ assertNotNull(id);
+ assertTrue(id > 0);
+
+ Optional inserted = routeNodeRepository.getById(id);
+ assertTrue(inserted.isPresent());
+ assertEquals("New Route Node", inserted.get().getName());
+ assertTrue(inserted.get().getDestination());
+ assertFalse(inserted.get().getIntermediate());
+ assertTrue(inserted.get().getSource());
+ assertFalse(inserted.get().getOutdated());
+ assertEquals("EXT-001", inserted.get().getExternalMappingId());
+ }
+
+ @Test
+ void testInsertWithNulls() {
+ // Given: Route node with nullable fields as null
+ RouteNode newNode = new RouteNode();
+ newNode.setName("Minimal Node");
+ newNode.setAddress("Address");
+ newNode.setGeoLat(new BigDecimal("50.0"));
+ newNode.setGeoLng(new BigDecimal("8.0"));
+ newNode.setDestination(false);
+ newNode.setIntermediate(false);
+ newNode.setSource(false);
+ newNode.setNodeId(null); // nullable FK
+ newNode.setUserNodeId(null); // nullable FK
+ newNode.setOutdated(false);
+ newNode.setCountryId(testCountryId);
+ newNode.setExternalMappingId("MIN-EXT"); // NOT NULL in schema
+
+ // When: Insert
+ Integer id = routeNodeRepository.insert(newNode);
+
+ // Then: Should be inserted with nullable FKs as null
+ assertNotNull(id);
+
+ Optional inserted = routeNodeRepository.getById(id);
+ assertTrue(inserted.isPresent());
+ assertEquals("Minimal Node", inserted.get().getName());
+ assertEquals("MIN-EXT", inserted.get().getExternalMappingId());
+ }
+
+ @Test
+ void testDeleteAllById() {
+ // Given: Multiple route nodes
+ Integer node1 = createRouteNode("Node 1", testNodeId, testCountryId, true, false, false);
+ Integer node2 = createRouteNode("Node 2", testNodeId, testCountryId, false, true, false);
+ Integer node3 = createRouteNode("Node 3", testNodeId, testCountryId, false, false, true);
+
+ // When: Delete first two
+ routeNodeRepository.deleteAllById(List.of(node1, node2));
+
+ // Then: Should delete specified nodes
+ assertFalse(routeNodeRepository.getById(node1).isPresent());
+ assertFalse(routeNodeRepository.getById(node2).isPresent());
+ assertTrue(routeNodeRepository.getById(node3).isPresent());
+ }
+
+ @Test
+ void testDeleteAllByIdEmpty() {
+ // Given: Some route nodes
+ Integer nodeId = createRouteNode("Node", testNodeId, testCountryId, true, false, false);
+
+ // When: Delete with empty list
+ routeNodeRepository.deleteAllById(List.of());
+
+ // Then: Should not delete anything
+ assertTrue(routeNodeRepository.getById(nodeId).isPresent());
+ }
+
+ @Test
+ void testDeleteAllByIdNull() {
+ // Given: Some route nodes
+ Integer nodeId = createRouteNode("Node", testNodeId, testCountryId, true, false, false);
+
+ // When: Delete with null
+ routeNodeRepository.deleteAllById(null);
+
+ // Then: Should not delete anything
+ assertTrue(routeNodeRepository.getById(nodeId).isPresent());
+ }
+
+ @Test
+ void testGetFromNodeBySectionId() {
+ // Given: Create route section with from and to nodes
+ Integer fromNodeId = createRouteNode("From Node", testNodeId, testCountryId, false, false, true);
+ Integer toNodeId = createRouteNode("To Node", testNodeId, testCountryId, true, false, false);
+ Integer sectionId = createRouteSection(testRouteId, fromNodeId, toNodeId);
+
+ // When: Get from node by section ID
+ Optional fromNode = routeNodeRepository.getFromNodeBySectionId(sectionId);
+
+ // Then: Should retrieve from node (verify by name and properties, not ID due to JOIN)
+ assertTrue(fromNode.isPresent());
+ assertEquals("From Node", fromNode.get().getName());
+ assertTrue(fromNode.get().getSource());
+ }
+
+ @Test
+ void testGetFromNodeBySectionIdNotFound() {
+ // When: Get from node for non-existent section
+ Optional fromNode = routeNodeRepository.getFromNodeBySectionId(99999);
+
+ // Then: Should return empty
+ assertFalse(fromNode.isPresent());
+ }
+
+ @Test
+ void testGetToNodeBySectionId() {
+ // Given: Create route section with from and to nodes
+ Integer fromNodeId = createRouteNode("From Node", testNodeId, testCountryId, false, false, true);
+ Integer toNodeId = createRouteNode("To Node", testNodeId, testCountryId, true, false, false);
+ Integer sectionId = createRouteSection(testRouteId, fromNodeId, toNodeId);
+
+ // When: Get to node by section ID
+ Optional toNode = routeNodeRepository.getToNodeBySectionId(sectionId);
+
+ // Then: Should retrieve to node (verify by name and properties, not ID due to JOIN)
+ assertTrue(toNode.isPresent());
+ assertEquals("To Node", toNode.get().getName());
+ assertTrue(toNode.get().getDestination());
+ }
+
+ @Test
+ void testGetToNodeBySectionIdNotFound() {
+ // When: Get to node for non-existent section
+ Optional toNode = routeNodeRepository.getToNodeBySectionId(99999);
+
+ // Then: Should return empty
+ assertFalse(toNode.isPresent());
+ }
+
+ @Test
+ void testBooleanFields() {
+ // Given: Create nodes with different boolean combinations
+ RouteNode node1 = new RouteNode();
+ node1.setName("All True");
+ node1.setAddress("Address 1");
+ node1.setGeoLat(new BigDecimal("50.0"));
+ node1.setGeoLng(new BigDecimal("8.0"));
+ node1.setDestination(true);
+ node1.setIntermediate(true);
+ node1.setSource(true);
+ node1.setOutdated(true);
+ node1.setCountryId(testCountryId);
+ node1.setExternalMappingId("EXT1");
+
+ RouteNode node2 = new RouteNode();
+ node2.setName("All False");
+ node2.setAddress("Address 2");
+ node2.setGeoLat(new BigDecimal("51.0"));
+ node2.setGeoLng(new BigDecimal("9.0"));
+ node2.setDestination(false);
+ node2.setIntermediate(false);
+ node2.setSource(false);
+ node2.setOutdated(false);
+ node2.setCountryId(testCountryId);
+ node2.setExternalMappingId("EXT2");
+
+ // When: Insert
+ Integer id1 = routeNodeRepository.insert(node1);
+ Integer id2 = routeNodeRepository.insert(node2);
+
+ // Then: Boolean values should be stored and retrieved correctly
+ Optional retrieved1 = routeNodeRepository.getById(id1);
+ assertTrue(retrieved1.isPresent());
+ assertTrue(retrieved1.get().getDestination());
+ assertTrue(retrieved1.get().getIntermediate());
+ assertTrue(retrieved1.get().getSource());
+ assertTrue(retrieved1.get().getOutdated());
+
+ Optional retrieved2 = routeNodeRepository.getById(id2);
+ assertTrue(retrieved2.isPresent());
+ assertFalse(retrieved2.get().getDestination());
+ assertFalse(retrieved2.get().getIntermediate());
+ assertFalse(retrieved2.get().getSource());
+ assertFalse(retrieved2.get().getOutdated());
+ }
+
+ @Test
+ void testGeoCoordinates() {
+ // Given: Node with specific coordinates
+ RouteNode node = new RouteNode();
+ node.setName("Geo Node");
+ node.setAddress("Geo Address");
+ node.setGeoLat(new BigDecimal("52.5200"));
+ node.setGeoLng(new BigDecimal("13.4050"));
+ node.setDestination(true);
+ node.setIntermediate(false);
+ node.setSource(false);
+ node.setCountryId(testCountryId);
+ node.setExternalMappingId("GEO1");
+
+ // When: Insert
+ Integer id = routeNodeRepository.insert(node);
+
+ // Then: Coordinates should be stored correctly
+ Optional retrieved = routeNodeRepository.getById(id);
+ assertTrue(retrieved.isPresent());
+ assertEquals(0, new BigDecimal("52.5200").compareTo(retrieved.get().getGeoLat()));
+ assertEquals(0, new BigDecimal("13.4050").compareTo(retrieved.get().getGeoLng()));
+ }
+
+ // ========== Helper Methods ==========
+
+ 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 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 createRoute(Integer destinationId) {
+ String sql = String.format(
+ "INSERT INTO premise_route (premise_destination_id, is_cheapest, is_fastest, is_selected) VALUES (?, %s, %s, %s)",
+ dialectProvider.getBooleanTrue(),
+ dialectProvider.getBooleanTrue(),
+ dialectProvider.getBooleanTrue());
+ executeRawSql(sql, destinationId);
+
+ String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
+ return jdbcTemplate.queryForObject(selectSql, Integer.class);
+ }
+
+ private Integer createRouteNode(String name, Integer nodeId, Integer countryId, boolean isDestination, boolean isIntermediate, boolean isSource) {
+ String sql = String.format(
+ "INSERT INTO premise_route_node (name, address, geo_lat, geo_lng, is_destination, is_intermediate, is_source, " +
+ "node_id, country_id, is_outdated, external_mapping_id) " +
+ "VALUES (?, 'Address', 51.5, 7.5, %s, %s, %s, ?, ?, %s, 'EXT')",
+ isDestination ? dialectProvider.getBooleanTrue() : dialectProvider.getBooleanFalse(),
+ isIntermediate ? dialectProvider.getBooleanTrue() : dialectProvider.getBooleanFalse(),
+ isSource ? dialectProvider.getBooleanTrue() : dialectProvider.getBooleanFalse(),
+ dialectProvider.getBooleanFalse());
+ executeRawSql(sql, name, 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, 'SEA', 'CONTAINER', %s, %s, %s, %s)",
+ dialectProvider.getBooleanFalse(),
+ dialectProvider.getBooleanTrue(),
+ dialectProvider.getBooleanFalse(),
+ dialectProvider.getBooleanFalse());
+ executeRawSql(sql, routeId, fromNodeId, toNodeId);
+
+ 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/premise/RouteRepositoryIntegrationTest.java b/src/test/java/de/avatic/lcc/repositories/premise/RouteRepositoryIntegrationTest.java
new file mode 100644
index 0000000..37fd6ae
--- /dev/null
+++ b/src/test/java/de/avatic/lcc/repositories/premise/RouteRepositoryIntegrationTest.java
@@ -0,0 +1,341 @@
+package de.avatic.lcc.repositories.premise;
+
+import de.avatic.lcc.model.db.premises.PremiseState;
+import de.avatic.lcc.model.db.premises.route.Route;
+import de.avatic.lcc.repositories.AbstractRepositoryIntegrationTest;
+import de.avatic.lcc.util.exception.internalerror.DatabaseException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Integration tests for RouteRepository.
+ *
+ * Tests critical functionality across both MySQL and MSSQL:
+ * - Boolean literals (TRUE/FALSE vs 1/0)
+ * - Dynamic IN clauses
+ * - Auto-generated keys
+ * - CRUD operations
+ *
+ * Run with:
+ *
+ * mvn test -Dspring.profiles.active=test,mysql -Dtest=RouteRepositoryIntegrationTest
+ * mvn test -Dspring.profiles.active=test,mssql -Dtest=RouteRepositoryIntegrationTest
+ *
+ */
+class RouteRepositoryIntegrationTest extends AbstractRepositoryIntegrationTest {
+
+ @Autowired
+ private RouteRepository routeRepository;
+
+ private Integer testUserId;
+ private Integer testCountryId;
+ private Integer testNodeId;
+ private Integer testMaterialId;
+ private Integer testPremiseId;
+ private Integer testDestinationId;
+
+ @BeforeEach
+ void setupTestData() {
+ // Clean up in correct order (respecting foreign key constraints)
+ jdbcTemplate.update("DELETE FROM premise_route_section");
+ 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 before deleting nodes
+ 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");
+
+ // Create test user
+ testUserId = createUser("WD001", "test@example.com");
+
+ // Get test country
+ testCountryId = getCountryId("DE");
+
+ // Create test node
+ testNodeId = createNode("Test Supplier", "SUP-001", testCountryId);
+
+ // Create test material
+ testMaterialId = createMaterial("Test Material", "MAT-001");
+
+ // Create test premise
+ testPremiseId = createPremise(testUserId, testNodeId, testMaterialId, testCountryId, PremiseState.DRAFT);
+
+ // Create test destination
+ testDestinationId = createDestination(testPremiseId, testNodeId);
+
+ // Create some test routes
+ createRoute(testDestinationId, true, true, true); // cheapest, fastest, selected
+ createRoute(testDestinationId, false, false, false); // not cheapest, not fastest, not selected
+ createRoute(testDestinationId, false, false, false); // not cheapest, not fastest, not selected
+ }
+
+ @Test
+ void testGetByDestinationId() {
+ // When: Get routes by destination
+ List routes = routeRepository.getByDestinationId(testDestinationId);
+
+ // Then: Should return all routes for destination
+ assertNotNull(routes);
+ assertEquals(3, routes.size());
+ assertTrue(routes.stream().allMatch(r -> r.getDestinationId().equals(testDestinationId)));
+ }
+
+ @Test
+ void testGetByDestinationIdEmpty() {
+ // When: Get routes for non-existent destination
+ List routes = routeRepository.getByDestinationId(99999);
+
+ // Then: Should return empty list
+ assertNotNull(routes);
+ assertTrue(routes.isEmpty());
+ }
+
+ @Test
+ void testGetSelectedByDestinationId() {
+ // When: Get selected route
+ Optional selected = routeRepository.getSelectedByDestinationId(testDestinationId);
+
+ // Then: Should return the selected route
+ assertTrue(selected.isPresent());
+ assertEquals(testDestinationId, selected.get().getDestinationId());
+ assertTrue(selected.get().getSelected());
+ assertTrue(selected.get().getCheapest());
+ assertTrue(selected.get().getFastest());
+ }
+
+ @Test
+ void testGetSelectedByDestinationIdNotFound() {
+ // Given: Destination with no selected routes
+ Integer destinationId2 = createDestination(testPremiseId, testNodeId);
+ createRoute(destinationId2, false, false, false);
+
+ // When: Get selected route
+ Optional selected = routeRepository.getSelectedByDestinationId(destinationId2);
+
+ // Then: Should return empty
+ assertFalse(selected.isPresent());
+ }
+
+ @Test
+ void testGetSelectedByDestinationIdMultipleThrows() {
+ // Given: Destination with multiple selected routes (invalid state)
+ Integer destinationId2 = createDestination(testPremiseId, testNodeId);
+ createRoute(destinationId2, false, false, true);
+ createRoute(destinationId2, false, false, true);
+
+ // When/Then: Should throw DatabaseException
+ assertThrows(DatabaseException.class, () ->
+ routeRepository.getSelectedByDestinationId(destinationId2));
+ }
+
+ @Test
+ void testInsert() {
+ // Given: New route
+ Route newRoute = new Route();
+ newRoute.setDestinationId(testDestinationId);
+ newRoute.setCheapest(false);
+ newRoute.setFastest(true);
+ newRoute.setSelected(false);
+
+ // When: Insert
+ Integer id = routeRepository.insert(newRoute);
+
+ // Then: Should be inserted
+ assertNotNull(id);
+ assertTrue(id > 0);
+
+ // Verify insertion
+ List routes = routeRepository.getByDestinationId(testDestinationId);
+ assertTrue(routes.stream().anyMatch(r -> r.getId().equals(id) && r.getFastest()));
+ }
+
+ @Test
+ void testDeleteAllById() {
+ // Given: Multiple routes
+ List routes = routeRepository.getByDestinationId(testDestinationId);
+ assertEquals(3, routes.size());
+
+ // Get first two route IDs
+ List idsToDelete = routes.stream()
+ .limit(2)
+ .map(Route::getId)
+ .toList();
+
+ // When: Delete by IDs
+ routeRepository.deleteAllById(idsToDelete);
+
+ // Then: Should delete specified routes
+ List remaining = routeRepository.getByDestinationId(testDestinationId);
+ assertEquals(1, remaining.size());
+ assertFalse(idsToDelete.contains(remaining.getFirst().getId()));
+ }
+
+ @Test
+ void testDeleteAllByIdEmpty() {
+ // When: Delete with empty list
+ routeRepository.deleteAllById(List.of());
+
+ // Then: Should not throw error, routes remain
+ List routes = routeRepository.getByDestinationId(testDestinationId);
+ assertEquals(3, routes.size());
+ }
+
+ @Test
+ void testDeleteAllByIdNull() {
+ // When: Delete with null
+ routeRepository.deleteAllById(null);
+
+ // Then: Should not throw error, routes remain
+ List routes = routeRepository.getByDestinationId(testDestinationId);
+ assertEquals(3, routes.size());
+ }
+
+ @Test
+ void testUpdateSelectedByDestinationId() {
+ // Given: Get non-selected route
+ List routes = routeRepository.getByDestinationId(testDestinationId);
+ Route nonSelectedRoute = routes.stream()
+ .filter(r -> !r.getSelected())
+ .findFirst()
+ .orElseThrow();
+
+ // When: Update selected route
+ routeRepository.updateSelectedByDestinationId(testDestinationId, nonSelectedRoute.getId());
+
+ // Then: New route should be selected, old route should be deselected
+ Optional newSelected = routeRepository.getSelectedByDestinationId(testDestinationId);
+ assertTrue(newSelected.isPresent());
+ assertEquals(nonSelectedRoute.getId(), newSelected.get().getId());
+ assertTrue(newSelected.get().getSelected());
+
+ // Verify only one route is selected
+ List allRoutes = routeRepository.getByDestinationId(testDestinationId);
+ long selectedCount = allRoutes.stream().filter(Route::getSelected).count();
+ assertEquals(1, selectedCount, "Only one route should be selected");
+ }
+
+ @Test
+ void testUpdateSelectedByDestinationIdInvalidRoute() {
+ // When/Then: Update with non-existent route ID should throw
+ assertThrows(DatabaseException.class, () ->
+ routeRepository.updateSelectedByDestinationId(testDestinationId, 99999));
+ }
+
+ @Test
+ void testBooleanLiterals() {
+ // Given: Create routes with different boolean values
+ Route route1 = new Route();
+ route1.setDestinationId(testDestinationId);
+ route1.setCheapest(true);
+ route1.setFastest(false);
+ route1.setSelected(true);
+
+ Route route2 = new Route();
+ route2.setDestinationId(testDestinationId);
+ route2.setCheapest(false);
+ route2.setFastest(true);
+ route2.setSelected(false);
+
+ // When: Insert
+ Integer id1 = routeRepository.insert(route1);
+ Integer id2 = routeRepository.insert(route2);
+
+ // Then: Boolean values should be stored and retrieved correctly
+ List routes = routeRepository.getByDestinationId(testDestinationId);
+
+ Route retrieved1 = routes.stream().filter(r -> r.getId().equals(id1)).findFirst().orElseThrow();
+ assertTrue(retrieved1.getCheapest());
+ assertFalse(retrieved1.getFastest());
+
+ Route retrieved2 = routes.stream().filter(r -> r.getId().equals(id2)).findFirst().orElseThrow();
+ assertFalse(retrieved2.getCheapest());
+ assertTrue(retrieved2.getFastest());
+ }
+
+ // ========== Helper Methods ==========
+
+ 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 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 createRoute(Integer destinationId, boolean isCheapest, boolean isFastest, boolean isSelected) {
+ String sql = String.format(
+ "INSERT INTO premise_route (premise_destination_id, is_cheapest, is_fastest, is_selected) VALUES (?, %s, %s, %s)",
+ isCheapest ? dialectProvider.getBooleanTrue() : dialectProvider.getBooleanFalse(),
+ isFastest ? dialectProvider.getBooleanTrue() : dialectProvider.getBooleanFalse(),
+ isSelected ? dialectProvider.getBooleanTrue() : dialectProvider.getBooleanFalse());
+ executeRawSql(sql, destinationId);
+
+ 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/premise/RouteSectionRepositoryIntegrationTest.java b/src/test/java/de/avatic/lcc/repositories/premise/RouteSectionRepositoryIntegrationTest.java
new file mode 100644
index 0000000..1cce056
--- /dev/null
+++ b/src/test/java/de/avatic/lcc/repositories/premise/RouteSectionRepositoryIntegrationTest.java
@@ -0,0 +1,427 @@
+package de.avatic.lcc.repositories.premise;
+
+import de.avatic.lcc.dto.generic.RateType;
+import de.avatic.lcc.dto.generic.TransportType;
+import de.avatic.lcc.model.db.premises.PremiseState;
+import de.avatic.lcc.model.db.premises.route.RouteSection;
+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.util.List;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Integration tests for RouteSectionRepository.
+ *
+ * Tests critical functionality across both MySQL and MSSQL:
+ * - Boolean literals (TRUE/FALSE vs 1/0)
+ * - Enum handling (transport_type, rate_type)
+ * - Dynamic IN clauses
+ * - NULL handling for optional fields
+ * - BigDecimal to Double conversion
+ *
+ * Run with:
+ *
+ * mvn test -Dspring.profiles.active=test,mysql -Dtest=RouteSectionRepositoryIntegrationTest
+ * mvn test -Dspring.profiles.active=test,mssql -Dtest=RouteSectionRepositoryIntegrationTest
+ *
+ */
+class RouteSectionRepositoryIntegrationTest extends AbstractRepositoryIntegrationTest {
+
+ @Autowired
+ private RouteSectionRepository routeSectionRepository;
+
+ private Integer testUserId;
+ private Integer testCountryId;
+ private Integer testNodeId;
+ private Integer testMaterialId;
+ private Integer testPremiseId;
+ private Integer testDestinationId;
+ private Integer testRouteId;
+ private Integer testFromNodeId;
+ private Integer testToNodeId;
+
+ @BeforeEach
+ void setupTestData() {
+ // Clean up in correct order (respecting foreign key constraints)
+ 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 before deleting nodes
+ 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");
+
+ // 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 premise
+ testPremiseId = createPremise(testUserId, testNodeId, testMaterialId, testCountryId, PremiseState.DRAFT);
+
+ // Create test destination
+ testDestinationId = createDestination(testPremiseId, testNodeId);
+
+ // Create test route
+ testRouteId = createRoute(testDestinationId);
+
+ // Create test route nodes
+ testFromNodeId = createRouteNode("From Node", testNodeId, testCountryId);
+ testToNodeId = createRouteNode("To Node", testNodeId, testCountryId);
+
+ // Create some test sections (respecting constraint: is_main_run must be TRUE unless transport_type is ROAD/POST_RUN)
+ createRouteSection(testRouteId, testFromNodeId, testToNodeId, 1, TransportType.SEA, RateType.CONTAINER, true, true, false);
+ createRouteSection(testRouteId, testFromNodeId, testToNodeId, 2, TransportType.ROAD, RateType.MATRIX, false, false, false); // ROAD allows is_main_run=FALSE
+ createRouteSection(testRouteId, testFromNodeId, testToNodeId, 3, TransportType.RAIL, RateType.NEAR_BY, false, true, true);
+ }
+
+ @Test
+ void testGetByRouteId() {
+ // When: Get sections by route ID
+ List sections = routeSectionRepository.getByRouteId(testRouteId);
+
+ // Then: Should return all sections for route
+ assertNotNull(sections);
+ assertEquals(3, sections.size());
+ assertTrue(sections.stream().allMatch(s -> s.getRouteId().equals(testRouteId)));
+ }
+
+ @Test
+ void testGetByRouteIdEmpty() {
+ // When: Get sections for non-existent route
+ List sections = routeSectionRepository.getByRouteId(99999);
+
+ // Then: Should return empty list
+ assertNotNull(sections);
+ assertTrue(sections.isEmpty());
+ }
+
+ @Test
+ void testGetById() {
+ // Given: Create route section
+ Integer sectionId = createRouteSection(testRouteId, testFromNodeId, testToNodeId, 10,
+ TransportType.SEA, RateType.CONTAINER, true, true, false);
+
+ // When: Get by ID
+ Optional section = routeSectionRepository.getById(sectionId);
+
+ // Then: Should retrieve
+ assertTrue(section.isPresent());
+ assertEquals(sectionId, section.get().getId());
+ assertEquals(testRouteId, section.get().getRouteId());
+ assertEquals(10, section.get().getListPosition());
+ assertEquals(TransportType.SEA, section.get().getTransportType());
+ assertEquals(RateType.CONTAINER, section.get().getRateType());
+ assertTrue(section.get().getPreRun());
+ assertTrue(section.get().getMainRun());
+ assertFalse(section.get().getPostRun());
+ }
+
+ @Test
+ void testGetByIdNotFound() {
+ // When: Get non-existent ID
+ Optional section = routeSectionRepository.getById(99999);
+
+ // Then: Should return empty
+ assertFalse(section.isPresent());
+ }
+
+ @Test
+ void testInsert() {
+ // Given: New route section
+ RouteSection newSection = new RouteSection();
+ newSection.setRouteId(testRouteId);
+ newSection.setFromRouteNodeId(testFromNodeId);
+ newSection.setToRouteNodeId(testToNodeId);
+ newSection.setListPosition(99);
+ newSection.setTransportType(TransportType.POST_RUN);
+ newSection.setRateType(RateType.MATRIX);
+ newSection.setPreRun(false);
+ newSection.setMainRun(true);
+ newSection.setPostRun(true);
+ newSection.setOutdated(false);
+ newSection.setDistance(250.5);
+
+ // When: Insert
+ Integer id = routeSectionRepository.insert(newSection);
+
+ // Then: Should be inserted
+ assertNotNull(id);
+ assertTrue(id > 0);
+
+ Optional inserted = routeSectionRepository.getById(id);
+ assertTrue(inserted.isPresent());
+ assertEquals(99, inserted.get().getListPosition());
+ assertEquals(TransportType.POST_RUN, inserted.get().getTransportType());
+ assertEquals(RateType.MATRIX, inserted.get().getRateType());
+ assertFalse(inserted.get().getPreRun());
+ assertTrue(inserted.get().getMainRun());
+ assertTrue(inserted.get().getPostRun());
+ assertNotNull(inserted.get().getDistance());
+ assertEquals(250.5, inserted.get().getDistance(), 0.01);
+ }
+
+ @Test
+ void testInsertWithNullDistance() {
+ // Given: Route section with null distance
+ RouteSection newSection = new RouteSection();
+ newSection.setRouteId(testRouteId);
+ newSection.setFromRouteNodeId(testFromNodeId);
+ newSection.setToRouteNodeId(testToNodeId);
+ newSection.setListPosition(50);
+ newSection.setTransportType(TransportType.SEA);
+ newSection.setRateType(RateType.CONTAINER);
+ newSection.setPreRun(true);
+ newSection.setMainRun(true); // Must be TRUE for SEA (constraint)
+ newSection.setPostRun(false);
+ newSection.setOutdated(false);
+ newSection.setDistance(null); // nullable
+
+ // When: Insert
+ Integer id = routeSectionRepository.insert(newSection);
+
+ // Then: Should be inserted with null distance
+ assertNotNull(id);
+
+ Optional inserted = routeSectionRepository.getById(id);
+ assertTrue(inserted.isPresent());
+ assertNull(inserted.get().getDistance());
+ }
+
+ @Test
+ void testDeleteAllById() {
+ // Given: Multiple route sections
+ List sections = routeSectionRepository.getByRouteId(testRouteId);
+ assertEquals(3, sections.size());
+
+ // Get first two section IDs
+ List idsToDelete = sections.stream()
+ .limit(2)
+ .map(RouteSection::getId)
+ .toList();
+
+ // When: Delete by IDs
+ routeSectionRepository.deleteAllById(idsToDelete);
+
+ // Then: Should delete specified sections
+ List remaining = routeSectionRepository.getByRouteId(testRouteId);
+ assertEquals(1, remaining.size());
+ assertFalse(idsToDelete.contains(remaining.getFirst().getId()));
+ }
+
+ @Test
+ void testDeleteAllByIdEmpty() {
+ // When: Delete with empty list
+ routeSectionRepository.deleteAllById(List.of());
+
+ // Then: Should not throw error, sections remain
+ List sections = routeSectionRepository.getByRouteId(testRouteId);
+ assertEquals(3, sections.size());
+ }
+
+ @Test
+ void testDeleteAllByIdNull() {
+ // When: Delete with null
+ routeSectionRepository.deleteAllById(null);
+
+ // Then: Should not throw error, sections remain
+ List sections = routeSectionRepository.getByRouteId(testRouteId);
+ assertEquals(3, sections.size());
+ }
+
+ @Test
+ void testTransportTypeEnum() {
+ // Given: Sections with different transport types
+ RouteSection section1 = new RouteSection();
+ section1.setRouteId(testRouteId);
+ section1.setFromRouteNodeId(testFromNodeId);
+ section1.setToRouteNodeId(testToNodeId);
+ section1.setListPosition(101);
+ section1.setTransportType(TransportType.RAIL);
+ section1.setRateType(RateType.CONTAINER);
+ section1.setPreRun(false);
+ section1.setMainRun(true); // Must be TRUE for RAIL (constraint)
+ section1.setPostRun(false);
+ section1.setOutdated(false);
+
+ RouteSection section2 = new RouteSection();
+ section2.setRouteId(testRouteId);
+ section2.setFromRouteNodeId(testFromNodeId);
+ section2.setToRouteNodeId(testToNodeId);
+ section2.setListPosition(102);
+ section2.setTransportType(TransportType.POST_RUN); // POST_RUN allows is_main_run=FALSE
+ section2.setRateType(RateType.NEAR_BY);
+ section2.setPreRun(false);
+ section2.setMainRun(false);
+ section2.setPostRun(true);
+ section2.setOutdated(false);
+
+ // When: Insert
+ Integer id1 = routeSectionRepository.insert(section1);
+ Integer id2 = routeSectionRepository.insert(section2);
+
+ // Then: Enum values should be stored and retrieved correctly
+ Optional retrieved1 = routeSectionRepository.getById(id1);
+ assertTrue(retrieved1.isPresent());
+ assertEquals(TransportType.RAIL, retrieved1.get().getTransportType());
+ assertEquals(RateType.CONTAINER, retrieved1.get().getRateType());
+
+ Optional retrieved2 = routeSectionRepository.getById(id2);
+ assertTrue(retrieved2.isPresent());
+ assertEquals(TransportType.POST_RUN, retrieved2.get().getTransportType());
+ assertEquals(RateType.NEAR_BY, retrieved2.get().getRateType());
+ }
+
+ @Test
+ void testBooleanFlags() {
+ // Given: Section with different boolean flags (respecting constraint)
+ RouteSection section = new RouteSection();
+ section.setRouteId(testRouteId);
+ section.setFromRouteNodeId(testFromNodeId);
+ section.setToRouteNodeId(testToNodeId);
+ section.setListPosition(200);
+ section.setTransportType(TransportType.ROAD); // ROAD allows is_main_run=FALSE
+ section.setRateType(RateType.CONTAINER);
+ section.setPreRun(true);
+ section.setMainRun(false);
+ section.setPostRun(true);
+ section.setOutdated(true);
+
+ // When: Insert
+ Integer id = routeSectionRepository.insert(section);
+
+ // Then: Boolean flags should be stored correctly
+ Optional retrieved = routeSectionRepository.getById(id);
+ assertTrue(retrieved.isPresent());
+ assertTrue(retrieved.get().getPreRun());
+ assertFalse(retrieved.get().getMainRun());
+ assertTrue(retrieved.get().getPostRun());
+ assertTrue(retrieved.get().getOutdated());
+ }
+
+ // ========== Helper Methods ==========
+
+ 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 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 createRoute(Integer destinationId) {
+ String sql = String.format(
+ "INSERT INTO premise_route (premise_destination_id, is_cheapest, is_fastest, is_selected) VALUES (?, %s, %s, %s)",
+ dialectProvider.getBooleanTrue(),
+ dialectProvider.getBooleanTrue(),
+ dialectProvider.getBooleanTrue());
+ executeRawSql(sql, destinationId);
+
+ String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
+ return jdbcTemplate.queryForObject(selectSql, Integer.class);
+ }
+
+ private Integer createRouteNode(String name, Integer nodeId, Integer countryId) {
+ String sql = String.format(
+ "INSERT INTO premise_route_node (name, address, geo_lat, geo_lng, is_destination, is_intermediate, is_source, " +
+ "node_id, country_id, is_outdated, external_mapping_id) " +
+ "VALUES (?, 'Address', 51.5, 7.5, %s, %s, %s, ?, ?, %s, 'EXT')",
+ dialectProvider.getBooleanTrue(),
+ dialectProvider.getBooleanTrue(),
+ dialectProvider.getBooleanTrue(),
+ dialectProvider.getBooleanFalse());
+ executeRawSql(sql, name, 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, int listPosition,
+ TransportType transportType, RateType rateType,
+ boolean isPreRun, boolean isMainRun, boolean isPostRun) {
+ 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 (?, ?, ?, ?, ?, ?, %s, %s, %s, %s)",
+ isPreRun ? dialectProvider.getBooleanTrue() : dialectProvider.getBooleanFalse(),
+ isMainRun ? dialectProvider.getBooleanTrue() : dialectProvider.getBooleanFalse(),
+ isPostRun ? dialectProvider.getBooleanTrue() : dialectProvider.getBooleanFalse(),
+ dialectProvider.getBooleanFalse());
+ executeRawSql(sql, routeId, fromNodeId, toNodeId, listPosition, transportType.name(), rateType.name());
+
+ String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)";
+ return jdbcTemplate.queryForObject(selectSql, Integer.class);
+ }
+}