package com.trafficaudit.holiday.service; import com.trafficaudit.holiday.HolidayConstants; import com.trafficaudit.holiday.HolidayNumberUtils; import com.trafficaudit.holiday.entity.HolidayCalendar; import com.trafficaudit.holiday.entity.HolidayPassengerFlow; import com.trafficaudit.holiday.entity.HolidayVehicleAdjustment; import com.trafficaudit.holiday.entity.HolidayVehicleFlow; import com.trafficaudit.holiday.mapper.HolidayPassengerFlowMapper; import com.trafficaudit.holiday.mapper.HolidayVehicleAdjustmentMapper; import com.trafficaudit.holiday.mapper.HolidayVehicleFlowMapper; import org.junit.jupiter.api.Test; import java.lang.reflect.Field; import java.math.BigDecimal; import java.time.LocalDate; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; /** * Ring-ratio denominators are built one day at a time. The Excel comparison base (column C) is * consumed on the source day and inherited by the next day's denominator, while historical * adjustment records change only that historical day's same-day value. */ class HolidayComparisonBaseTest { @Test void comparisonBaseChangesOnlyTheNextDayRingRatioDenominator() throws Exception { HolidayStatService service = serviceWithData( 2026, passengerRows(2026, "50", "10", "6", "3"), vehicleRows(2026), Collections.emptyList()); List> rows = service.buildOverviewRows( 2026, HolidayConstants.TYPE_NATIONAL_DAY, calendar(2026)); assertEquals(new BigDecimal("323.7837"), rows.get(2).get("nationalProvincial")); assertEquals(new BigDecimal("320.2418"), rows.get(2).get("comparisonBase")); assertEquals(new BigDecimal("322.7695"), rows.get(3).get("nationalProvincial")); BigDecimal nationalRatio = (BigDecimal) rows.get(3).get("nationalProvincialRingRatio"); assertEquals(ratio(new BigDecimal("322.7695"), new BigDecimal("320.2418")), nationalRatio); assertNotEquals(ratio(new BigDecimal("322.7695"), new BigDecimal("323.7837")), nationalRatio); HolidayStatService.DailyMetrics base = service.ringRatioBaseMetrics( 2026, HolidayConstants.TYPE_NATIONAL_DAY, 4); assertNotNull(base); assertEquals(new BigDecimal("320.2418"), base.getNationalProvincial()); assertDecimalEquals("1008.58032", base.getNonBusinessPersonFlow()); assertDecimalEquals("1058.58032", base.getRoadPersonFlow()); assertDecimalEquals("1077.58032", base.getTotalPersonFlow()); } @Test void historicalAdjustmentChangesTheHistoricalSameDayValue() throws Exception { HolidayVehicleAdjustment adjustment = new HolidayVehicleAdjustment(); adjustment.setYear(2025); adjustment.setFlowDate(LocalDate.of(2025, 10, 4)); adjustment.setAdjustedValue(new BigDecimal("337.5009")); HolidayStatService service = serviceWithData( 2025, passengerRows(2025, "50", "10", "6", "3"), Arrays.asList( vehicle("2025-10-03", HolidayConstants.ROAD_EXPRESSWAY, "100", null), vehicle("2025-10-03", HolidayConstants.ROAD_NATIONAL_PROVINCIAL, "362.0035", null), vehicle("2025-10-04", HolidayConstants.ROAD_EXPRESSWAY, "110", null), vehicle("2025-10-04", HolidayConstants.ROAD_NATIONAL_PROVINCIAL, "350.0000", null)), Collections.singletonList(adjustment)); List> rows = service.buildOverviewRows( 2025, HolidayConstants.TYPE_NATIONAL_DAY, calendar(2025)); assertEquals(new BigDecimal("337.5009"), rows.get(3).get("nationalProvincial")); } private static HolidayStatService serviceWithData(int year, List passengerRows, List vehicleRows, List adjustments) throws Exception { HolidayConfigService configService = mock(HolidayConfigService.class); HolidayPassengerFlowMapper passengerMapper = mock(HolidayPassengerFlowMapper.class); HolidayVehicleFlowMapper vehicleMapper = mock(HolidayVehicleFlowMapper.class); HolidayVehicleAdjustmentMapper adjustmentMapper = mock(HolidayVehicleAdjustmentMapper.class); when(configService.coefficientOf(year, HolidayConstants.CATEGORY_HOLIDAY)).thenReturn(new BigDecimal("2.4")); when(configService.findHolidayDays(year, HolidayConstants.TYPE_NATIONAL_DAY)).thenReturn(calendar(year)); when(passengerMapper.selectList(any())).thenReturn(passengerRows); when(vehicleMapper.selectList(any())).thenReturn(vehicleRows); when(adjustmentMapper.selectList(any())).thenReturn(adjustments); HolidayStatService service = new HolidayStatService(); inject(service, "configService", configService); inject(service, "passengerFlowMapper", passengerMapper); inject(service, "vehicleFlowMapper", vehicleMapper); inject(service, "vehicleAdjustmentMapper", adjustmentMapper); return service; } private static BigDecimal ratio(BigDecimal current, BigDecimal base) { return HolidayNumberUtils.normalize(HolidayNumberUtils.divide(current, base).subtract(BigDecimal.ONE)); } private static void assertDecimalEquals(String expected, BigDecimal actual) { assertNotNull(actual); assertEquals(0, new BigDecimal(expected).compareTo(actual)); } private static List vehicleRows(int year) { List rows = new ArrayList<>(); rows.add(vehicle(year + "-10-03", HolidayConstants.ROAD_EXPRESSWAY, "100", null)); rows.add(vehicle(year + "-10-03", HolidayConstants.ROAD_NATIONAL_PROVINCIAL, "323.7837", "320.2418")); rows.add(vehicle(year + "-10-04", HolidayConstants.ROAD_EXPRESSWAY, "110", null)); rows.add(vehicle(year + "-10-04", HolidayConstants.ROAD_NATIONAL_PROVINCIAL, "322.7695", null)); return rows; } private static HolidayVehicleFlow vehicle(String date, String roadCategory, String value, String base) { HolidayVehicleFlow row = new HolidayVehicleFlow(); row.setYear(LocalDate.parse(date).getYear()); row.setFlowDate(LocalDate.parse(date)); row.setRoadCategory(roadCategory); row.setVehicleType(HolidayConstants.VEHICLE_MEDIUM_SMALL_PASSENGER); row.setVehicleCount(new BigDecimal(value)); row.setComparisonBase(base == null ? null : new BigDecimal(base)); return row; } private static List passengerRows(int year, String road, String rail, String civilAviation, String water) { List rows = new ArrayList<>(); rows.add(passenger(year + "-10-03", HolidayConstants.MODE_ROAD, road)); rows.add(passenger(year + "-10-03", HolidayConstants.MODE_RAIL, rail)); rows.add(passenger(year + "-10-03", HolidayConstants.MODE_CIVIL_AVIATION, civilAviation)); rows.add(passenger(year + "-10-03", HolidayConstants.MODE_WATER, water)); rows.add(passenger(year + "-10-04", HolidayConstants.MODE_ROAD, road)); rows.add(passenger(year + "-10-04", HolidayConstants.MODE_RAIL, rail)); rows.add(passenger(year + "-10-04", HolidayConstants.MODE_CIVIL_AVIATION, civilAviation)); rows.add(passenger(year + "-10-04", HolidayConstants.MODE_WATER, water)); return rows; } private static HolidayPassengerFlow passenger(String date, String mode, String value) { HolidayPassengerFlow row = new HolidayPassengerFlow(); row.setYear(LocalDate.parse(date).getYear()); row.setFlowDate(LocalDate.parse(date)); row.setTransportMode(mode); row.setPassengerCount(new BigDecimal(value)); return row; } private static List calendar(int year) { List days = new ArrayList<>(); for (int i = 1; i <= 4; i++) { HolidayCalendar day = new HolidayCalendar(); day.setYear(year); day.setHolidayType(HolidayConstants.TYPE_NATIONAL_DAY); day.setDayIndex(i); day.setHolidayDate(LocalDate.of(year, 10, i)); days.add(day); } return days; } private static void inject(Object target, String field, Object value) throws Exception { Field declared = target.getClass().getDeclaredField(field); declared.setAccessible(true); declared.set(target, value); } }