diff --git a/server/api/src/main/java/com/hanwha/nexacrodemo/report/ReportService.java b/server/api/src/main/java/com/hanwha/nexacrodemo/report/ReportService.java index 47686ae..a3a3f39 100644 --- a/server/api/src/main/java/com/hanwha/nexacrodemo/report/ReportService.java +++ b/server/api/src/main/java/com/hanwha/nexacrodemo/report/ReportService.java @@ -201,11 +201,11 @@ public class ReportService { Row titleRow = sheet.createRow(0); titleRow.setHeightInPoints(24); createCell(titleRow, 0, sheetName + " Detail", styles.get("title")); - sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, Math.max(0, columnCount - 1))); + mergeIfNeeded(sheet, 0, columnCount); Row subtitleRow = sheet.createRow(1); createCell(subtitleRow, 0, "Fiscal period " + fiscalPeriod + " | Rows " + rows.size(), styles.get("subtitle")); - sheet.addMergedRegion(new CellRangeAddress(1, 1, 0, Math.max(0, columnCount - 1))); + mergeIfNeeded(sheet, 1, columnCount); if (rows.isEmpty()) { Row emptyRow = sheet.createRow(3); @@ -347,6 +347,12 @@ public class ReportService { cell.setCellStyle(style); } + private void mergeIfNeeded(Sheet sheet, int rowIndex, int columnCount) { + if (columnCount > 1) { + sheet.addMergedRegion(new CellRangeAddress(rowIndex, rowIndex, 0, columnCount - 1)); + } + } + private CellStyle styleForKey(Map styles, String key) { return key.toLowerCase().contains("ratio") ? styles.get("ratio") : styles.get("number"); } diff --git a/server/api/src/test/java/com/hanwha/nexacrodemo/consolidation/ConsolidationIntegrationTest.java b/server/api/src/test/java/com/hanwha/nexacrodemo/consolidation/ConsolidationIntegrationTest.java index d97fd9a..a675241 100644 --- a/server/api/src/test/java/com/hanwha/nexacrodemo/consolidation/ConsolidationIntegrationTest.java +++ b/server/api/src/test/java/com/hanwha/nexacrodemo/consolidation/ConsolidationIntegrationTest.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.hanwha.nexacrodemo.upload.TestWorkbookFactory; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import org.apache.poi.ss.usermodel.WorkbookFactory; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -20,12 +21,14 @@ import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpSession; import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; @SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("test") +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) class ConsolidationIntegrationTest { @Autowired @@ -39,24 +42,27 @@ class ConsolidationIntegrationTest { @Test void validUploadsCanBeConsolidatedAndReported() throws Exception { + String fiscalPeriod = "2026-04"; MockHttpSession session = login("operator", "demo1234"); - upload(session, "trial-balance", "tb-valid.xlsx", TestWorkbookFactory.trialBalanceValid()); - upload(session, "forecast", "forecast-valid.xlsx", TestWorkbookFactory.forecastValid()); + upload(session, "trial-balance", fiscalPeriod, "tb-valid.xlsx", withFiscalPeriod(TestWorkbookFactory.trialBalanceValid(), fiscalPeriod)); + upload(session, "forecast", fiscalPeriod, "forecast-valid.xlsx", withFiscalPeriod(TestWorkbookFactory.forecastValid(), fiscalPeriod)); MvcResult runResult = mockMvc.perform(post("/api/consolidations/runs") .contentType(MediaType.APPLICATION_JSON) .session(session) .content(""" { - "fiscalPeriod": "2026-03", + "fiscalPeriod": "%s", "reportCurrency": "KRW" } - """)) + """.formatted(fiscalPeriod))) .andExpect(status().isOk()) .andExpect(jsonPath("$.statusCode").value("REQUESTED")) .andReturn(); + long runId = objectMapper.readTree(runResult.getResponse().getContentAsByteArray()).path("id").asLong(); + consolidationService.processPendingRuns("test-worker"); mockMvc.perform(get("/api/tx/consolidations/overview").session(session)) @@ -65,20 +71,24 @@ class ConsolidationIntegrationTest { MvcResult overviewResult = mockMvc.perform(get("/api/tx/reports/overview").session(session)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.datasets.artifacts.length()").value(2)) .andExpect(jsonPath("$.datasets.jobLogs[0].logLevel").exists()) .andReturn(); JsonNode artifacts = objectMapper.readTree(overviewResult.getResponse().getContentAsByteArray()) .path("datasets") .path("artifacts"); + int artifactCountForRun = 0; long excelArtifactId = -1L; for (JsonNode artifact : artifacts) { + if (artifact.path("runId").asLong() != runId) { + continue; + } + artifactCountForRun++; if ("EXCEL".equals(artifact.path("artifactType").asText())) { excelArtifactId = artifact.path("id").asLong(); - break; } } + Assertions.assertEquals(2, artifactCountForRun); Assertions.assertTrue(excelArtifactId > 0, "EXCEL artifact should exist"); MvcResult excelDownload = mockMvc.perform(get("/api/reports/{artifactId}/download", excelArtifactId).session(session)) @@ -92,7 +102,61 @@ class ConsolidationIntegrationTest { } } - private void upload(MockHttpSession session, String templateCode, String fileName, byte[] content) throws Exception { + @Test + void consolidationStillSucceedsWhenForecastSectionIsEmpty() throws Exception { + String fiscalPeriod = "2026-03"; + MockHttpSession session = login("operator", "demo1234"); + + upload(session, "trial-balance", fiscalPeriod, "tb-valid.xlsx", TestWorkbookFactory.trialBalanceValid()); + + MvcResult runResult = mockMvc.perform(post("/api/consolidations/runs") + .contentType(MediaType.APPLICATION_JSON) + .session(session) + .content(""" + { + "fiscalPeriod": "%s", + "reportCurrency": "KRW" + } + """.formatted(fiscalPeriod))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode").value("REQUESTED")) + .andReturn(); + + long runId = objectMapper.readTree(runResult.getResponse().getContentAsByteArray()).path("id").asLong(); + + consolidationService.processPendingRuns("test-worker"); + + MvcResult overviewResult = mockMvc.perform(get("/api/tx/reports/overview").session(session)) + .andExpect(status().isOk()) + .andReturn(); + + JsonNode artifacts = objectMapper.readTree(overviewResult.getResponse().getContentAsByteArray()) + .path("datasets") + .path("artifacts"); + int artifactCountForRun = 0; + long excelArtifactId = -1L; + for (JsonNode artifact : artifacts) { + if (artifact.path("runId").asLong() != runId) { + continue; + } + artifactCountForRun++; + if ("EXCEL".equals(artifact.path("artifactType").asText())) { + excelArtifactId = artifact.path("id").asLong(); + } + } + Assertions.assertEquals(2, artifactCountForRun); + Assertions.assertTrue(excelArtifactId > 0, "EXCEL artifact should exist"); + + MvcResult excelDownload = mockMvc.perform(get("/api/reports/{artifactId}/download", excelArtifactId).session(session)) + .andExpect(status().isOk()) + .andReturn(); + + try (var workbook = WorkbookFactory.create(new ByteArrayInputStream(excelDownload.getResponse().getContentAsByteArray()))) { + Assertions.assertEquals("No data available", workbook.getSheet("Forecast").getRow(3).getCell(0).getStringCellValue()); + } + } + + private void upload(MockHttpSession session, String templateCode, String fiscalPeriod, String fileName, byte[] content) throws Exception { MockMultipartFile file = new MockMultipartFile( "file", fileName, @@ -103,12 +167,27 @@ class ConsolidationIntegrationTest { mockMvc.perform(multipart("/api/uploads") .file(file) .param("templateCode", templateCode) - .param("fiscalPeriod", "2026-03") + .param("fiscalPeriod", fiscalPeriod) .session(session)) .andExpect(status().isOk()) .andExpect(jsonPath("$.statusCode").value("ACCEPTED")); } + private byte[] withFiscalPeriod(byte[] content, String fiscalPeriod) throws Exception { + try (var workbook = WorkbookFactory.create(new ByteArrayInputStream(content))) { + var dataSheet = workbook.getSheet("DATA"); + for (int rowIndex = 1; rowIndex <= dataSheet.getLastRowNum(); rowIndex++) { + var row = dataSheet.getRow(rowIndex); + if (row != null && row.getCell(0) != null) { + row.getCell(0).setCellValue(fiscalPeriod); + } + } + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + workbook.write(outputStream); + return outputStream.toByteArray(); + } + } + private MockHttpSession login(String username, String password) throws Exception { MvcResult result = mockMvc.perform(post("/api/auth/login") .contentType(MediaType.APPLICATION_JSON)