From a5ddff07b9414c0c1a882e61eda68df595c38786 Mon Sep 17 00:00:00 2001 From: v1tt0ri0Alt Date: Fri, 13 Feb 2026 17:34:26 +0100 Subject: [PATCH 1/2] Added downloadTransactionsReport in ReportController --- .../controller/ReportController.java | 13 +- .../controller/ReportControllerImpl.java | 24 +- .../dto/DownloadReportResponseDTO.java | 11 + .../repository/ReportRepository.java | 6 + .../transactions/service/ReportService.java | 9 + .../service/ReportServiceImpl.java | 88 +++++- .../storage/BlobStorageClientConfig.java | 4 + .../storage/BlobStorageProperties.java | 1 + .../storage/ReportBlobService.java | 5 + .../storage/ReportBlobServiceImpl.java | 20 ++ .../utils/ExceptionConstants.java | 4 + .../controller/ReportControllerImplTest.java | 109 ++++++- .../service/ReportServiceImplTest.java | 280 +++++++++++++++++- .../storage/BlobStorageClientConfigTest.java | 15 + .../storage/ReportBlobServiceImplTest.java | 117 ++++++++ 15 files changed, 689 insertions(+), 17 deletions(-) create mode 100644 src/main/java/it/gov/pagopa/idpay/transactions/dto/DownloadReportResponseDTO.java create mode 100644 src/main/java/it/gov/pagopa/idpay/transactions/storage/ReportBlobService.java create mode 100644 src/main/java/it/gov/pagopa/idpay/transactions/storage/ReportBlobServiceImpl.java create mode 100644 src/test/java/it/gov/pagopa/idpay/transactions/storage/ReportBlobServiceImplTest.java diff --git a/src/main/java/it/gov/pagopa/idpay/transactions/controller/ReportController.java b/src/main/java/it/gov/pagopa/idpay/transactions/controller/ReportController.java index b32c4c02..5e812006 100644 --- a/src/main/java/it/gov/pagopa/idpay/transactions/controller/ReportController.java +++ b/src/main/java/it/gov/pagopa/idpay/transactions/controller/ReportController.java @@ -1,9 +1,6 @@ package it.gov.pagopa.idpay.transactions.controller; -import it.gov.pagopa.idpay.transactions.dto.PatchReportRequest; -import it.gov.pagopa.idpay.transactions.dto.ReportDTO; -import it.gov.pagopa.idpay.transactions.dto.ReportListDTO; -import it.gov.pagopa.idpay.transactions.dto.ReportRequest; +import it.gov.pagopa.idpay.transactions.dto.*; import jakarta.validation.Valid; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; @@ -21,6 +18,14 @@ Mono getTransactionsReports( @PageableDefault Pageable pageable ); + @GetMapping("/initiatives/{initiativeId}/reports/{reportId}/download") + Mono downloadTransactionsReport( + @RequestHeader(value = "x-merchant-id", required = false) String merchantId, + @RequestHeader(value = "x-organization-role", required = false) String organizationRole, + @PathVariable("initiativeId") String initiativeId, + @PathVariable("reportId") String reportId + ); + @PostMapping("/initiatives/{initiativeId}/reports") Mono generateReport(@RequestHeader("x-merchant-id") String merchantId, @RequestHeader(value = "x-organization-role", required = false) String organizationRole, diff --git a/src/main/java/it/gov/pagopa/idpay/transactions/controller/ReportControllerImpl.java b/src/main/java/it/gov/pagopa/idpay/transactions/controller/ReportControllerImpl.java index fe6c6505..82d35d6f 100644 --- a/src/main/java/it/gov/pagopa/idpay/transactions/controller/ReportControllerImpl.java +++ b/src/main/java/it/gov/pagopa/idpay/transactions/controller/ReportControllerImpl.java @@ -1,9 +1,6 @@ package it.gov.pagopa.idpay.transactions.controller; -import it.gov.pagopa.idpay.transactions.dto.PatchReportRequest; -import it.gov.pagopa.idpay.transactions.dto.ReportDTO; -import it.gov.pagopa.idpay.transactions.dto.ReportListDTO; -import it.gov.pagopa.idpay.transactions.dto.ReportRequest; +import it.gov.pagopa.idpay.transactions.dto.*; import it.gov.pagopa.idpay.transactions.service.ReportService; import it.gov.pagopa.idpay.transactions.dto.mapper.ReportMapper; import it.gov.pagopa.idpay.transactions.utils.Utilities; @@ -37,6 +34,25 @@ public Mono getTransactionsReports( .flatMap(page -> Mono.just(reportMapper.toListDTO(page))); } + @Override + public Mono downloadTransactionsReport( + String merchantId, + String organizationRole, + String initiativeId, + String reportId + ) { + log.info("[DOWNLOAD_TRANSACTIONS_REPORT] Request received for initiative: {}, reportId: {}", + Utilities.sanitizeString(initiativeId), + Utilities.sanitizeString(reportId)); + + return reportService.downloadTransactionsReport( + merchantId, + organizationRole, + initiativeId, + reportId + ); + } + @Override public Mono generateReport(String merchantId, diff --git a/src/main/java/it/gov/pagopa/idpay/transactions/dto/DownloadReportResponseDTO.java b/src/main/java/it/gov/pagopa/idpay/transactions/dto/DownloadReportResponseDTO.java new file mode 100644 index 00000000..c3573c28 --- /dev/null +++ b/src/main/java/it/gov/pagopa/idpay/transactions/dto/DownloadReportResponseDTO.java @@ -0,0 +1,11 @@ +package it.gov.pagopa.idpay.transactions.dto; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class DownloadReportResponseDTO { + + private String reportUrl; +} diff --git a/src/main/java/it/gov/pagopa/idpay/transactions/repository/ReportRepository.java b/src/main/java/it/gov/pagopa/idpay/transactions/repository/ReportRepository.java index b76e06d7..65acf042 100644 --- a/src/main/java/it/gov/pagopa/idpay/transactions/repository/ReportRepository.java +++ b/src/main/java/it/gov/pagopa/idpay/transactions/repository/ReportRepository.java @@ -7,4 +7,10 @@ public interface ReportRepository extends ReactiveMongoRepository, ReportSpecificRepository { Mono findByIdAndInitiativeId(String reportId, String initiativeId); + + Mono findByIdAndInitiativeIdAndMerchantId( + String reportId, + String initiativeId, + String merchantId + ); } diff --git a/src/main/java/it/gov/pagopa/idpay/transactions/service/ReportService.java b/src/main/java/it/gov/pagopa/idpay/transactions/service/ReportService.java index 5c789ffc..68876fc3 100644 --- a/src/main/java/it/gov/pagopa/idpay/transactions/service/ReportService.java +++ b/src/main/java/it/gov/pagopa/idpay/transactions/service/ReportService.java @@ -1,5 +1,6 @@ package it.gov.pagopa.idpay.transactions.service; +import it.gov.pagopa.idpay.transactions.dto.DownloadReportResponseDTO; import it.gov.pagopa.idpay.transactions.dto.PatchReportRequest; import it.gov.pagopa.idpay.transactions.dto.ReportDTO; import it.gov.pagopa.idpay.transactions.dto.ReportRequest; @@ -19,4 +20,12 @@ Mono generateReport(String merchantId, Mono patchReport(String initiativeId, String reportId, PatchReportRequest request); + + Mono downloadTransactionsReport( + String merchantId, + String organizationRole, + String initiativeId, + String reportId + ); + } diff --git a/src/main/java/it/gov/pagopa/idpay/transactions/service/ReportServiceImpl.java b/src/main/java/it/gov/pagopa/idpay/transactions/service/ReportServiceImpl.java index 9eb8b625..9fe73c3b 100644 --- a/src/main/java/it/gov/pagopa/idpay/transactions/service/ReportServiceImpl.java +++ b/src/main/java/it/gov/pagopa/idpay/transactions/service/ReportServiceImpl.java @@ -1,6 +1,7 @@ package it.gov.pagopa.idpay.transactions.service; import it.gov.pagopa.common.web.exception.ClientExceptionWithBody; +import it.gov.pagopa.idpay.transactions.dto.DownloadReportResponseDTO; import it.gov.pagopa.idpay.transactions.dto.PatchReportRequest; import it.gov.pagopa.idpay.transactions.dto.ReportDTO; import it.gov.pagopa.idpay.transactions.dto.ReportRequest; @@ -10,6 +11,7 @@ import it.gov.pagopa.idpay.transactions.enums.RewardBatchAssignee; import it.gov.pagopa.idpay.transactions.model.Report; import it.gov.pagopa.idpay.transactions.repository.ReportRepository; +import it.gov.pagopa.idpay.transactions.storage.ReportBlobService; import it.gov.pagopa.idpay.transactions.utils.Utilities; import it.gov.pagopa.idpay.transactions.connector.rest.MerchantRestClient; import lombok.extern.slf4j.Slf4j; @@ -23,7 +25,6 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.List; -import java.util.Locale; import static it.gov.pagopa.idpay.transactions.utils.ExceptionConstants.ExceptionCode.*; import static it.gov.pagopa.idpay.transactions.utils.ExceptionConstants.ExceptionMessage.*; @@ -39,13 +40,16 @@ public class ReportServiceImpl implements ReportService { private final ReportMapper reportMapper; - public ReportServiceImpl(ReportRepository reportRepository, MerchantRestClient merchantRestClient, ReportMapper reportMapper) { + private final ReportBlobService reportBlobService; + + public ReportServiceImpl(ReportRepository reportRepository, MerchantRestClient merchantRestClient, ReportMapper reportMapper, ReportBlobService reportBlobService) { this.reportRepository = reportRepository; this.merchantRestClient = merchantRestClient; this.reportMapper = reportMapper; + this.reportBlobService = reportBlobService; } - private static final List ALLOWED_ROLES = List.of( + static final List ALLOWED_ROLES = List.of( "operator1", "operator2", "operator3" ); private static final DateTimeFormatter FILE_NAME_FORMAT = DateTimeFormatter.ofPattern("ddMMyyyyHHmmss"); @@ -193,4 +197,82 @@ public Mono patchReport(String initiativeId, } + @Override + public Mono downloadTransactionsReport( + String merchantId, + String organizationRole, + String initiativeId, + String reportId + ) { + + if ((merchantId == null || merchantId.isBlank()) && + (organizationRole == null || organizationRole.isBlank())) { + + return Mono.error(new ClientExceptionWithBody( + HttpStatus.BAD_REQUEST, + MERCHANT_ID_OR_ORGANIZATION_ROLE_ARE_MANDATORY, + ERROR_MESSAGE_MERCHANT_ID_OR_ORGANIZATION_ROLE_ARE_MANDATORY + )); + } + + if (merchantId != null && organizationRole != null) { + return Mono.error(new ClientExceptionWithBody( + HttpStatus.BAD_REQUEST, + MERCHANT_ID_AND_ORGANIZATION_ROLE_CANNOT_COEXIST, + ERROR_MESSAGE_MERCHANT_ID_AND_ORGANIZATION_ROLE_CANNOT_COEXIST + )); + } + + if (organizationRole != null && + ALLOWED_ROLES.stream().noneMatch(role -> role.equalsIgnoreCase(organizationRole))) { + + return Mono.error(new ClientExceptionWithBody( + HttpStatus.BAD_REQUEST, + INVALID_ORGANIZATION_ROLE, + ERROR_MESSAGE_INVALID_ORGANIZATION_ROLE + )); + } + + Mono query = merchantId == null + ? reportRepository.findByIdAndInitiativeId(reportId, initiativeId) + : reportRepository.findByIdAndInitiativeIdAndMerchantId(reportId, initiativeId, merchantId); + + return query + .switchIfEmpty(Mono.error(new ClientExceptionWithBody( + HttpStatus.NOT_FOUND, + REPORT_NOT_FOUND, + ERROR_MESSAGE_REPORT_NOT_FOUND.formatted(reportId, initiativeId) + ))) + .map(report -> { + + if (!ReportStatus.GENERATED.equals(report.getReportStatus())) { + throw new ClientExceptionWithBody( + HttpStatus.BAD_REQUEST, + REPORT_NOT_GENERATED, + ERROR_MESSAGE_REPORT_NOT_GENERATED.formatted(reportId) + ); + } + + String filename = report.getFileName(); + if (filename == null || filename.isBlank()) { + throw new ClientExceptionWithBody( + HttpStatus.INTERNAL_SERVER_ERROR, + REPORT_MISSING_FILENAME, + ERROR_MESSAGE_REPORT_MISSING_FILENAME.formatted(reportId) + ); + } + + String blobPath = String.format( + "/initiative/%s/merchant/%s/report/%s", + initiativeId, + report.getMerchantId(), + filename + ); + + return DownloadReportResponseDTO.builder() + .reportUrl(reportBlobService.getFileSignedUrl(blobPath)) + .build(); + }); + } + } diff --git a/src/main/java/it/gov/pagopa/idpay/transactions/storage/BlobStorageClientConfig.java b/src/main/java/it/gov/pagopa/idpay/transactions/storage/BlobStorageClientConfig.java index 1fd1bb3d..71da0bf9 100644 --- a/src/main/java/it/gov/pagopa/idpay/transactions/storage/BlobStorageClientConfig.java +++ b/src/main/java/it/gov/pagopa/idpay/transactions/storage/BlobStorageClientConfig.java @@ -34,4 +34,8 @@ public BlobContainerClient rewardBatchesContainerClient(BlobServiceClient blobSe return blobServiceClient.getBlobContainerClient(properties.getCsvContainerReference()); } + @Bean("reportsContainerClient") + public BlobContainerClient reportsContainerClient(BlobServiceClient blobServiceClient){ + return blobServiceClient.getBlobContainerClient(properties.getReportsContainerReference()); + } } diff --git a/src/main/java/it/gov/pagopa/idpay/transactions/storage/BlobStorageProperties.java b/src/main/java/it/gov/pagopa/idpay/transactions/storage/BlobStorageProperties.java index d556e97f..0271bbae 100644 --- a/src/main/java/it/gov/pagopa/idpay/transactions/storage/BlobStorageProperties.java +++ b/src/main/java/it/gov/pagopa/idpay/transactions/storage/BlobStorageProperties.java @@ -13,4 +13,5 @@ public class BlobStorageProperties { private String containerReference; private String csvContainerReference; private Integer invoiceTokenDurationSeconds; + private String reportsContainerReference; } diff --git a/src/main/java/it/gov/pagopa/idpay/transactions/storage/ReportBlobService.java b/src/main/java/it/gov/pagopa/idpay/transactions/storage/ReportBlobService.java new file mode 100644 index 00000000..312d7a53 --- /dev/null +++ b/src/main/java/it/gov/pagopa/idpay/transactions/storage/ReportBlobService.java @@ -0,0 +1,5 @@ +package it.gov.pagopa.idpay.transactions.storage; + +public interface ReportBlobService { + String getFileSignedUrl(String blobPath); +} diff --git a/src/main/java/it/gov/pagopa/idpay/transactions/storage/ReportBlobServiceImpl.java b/src/main/java/it/gov/pagopa/idpay/transactions/storage/ReportBlobServiceImpl.java new file mode 100644 index 00000000..c89faafd --- /dev/null +++ b/src/main/java/it/gov/pagopa/idpay/transactions/storage/ReportBlobServiceImpl.java @@ -0,0 +1,20 @@ +package it.gov.pagopa.idpay.transactions.storage; + +import com.azure.storage.blob.BlobContainerClient; +import com.azure.storage.blob.BlobServiceClient; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class ReportBlobServiceImpl extends AbstractBlobStorageClient implements ReportBlobService { + + public ReportBlobServiceImpl( + BlobServiceClient blobServiceClient, + @Qualifier("reportsContainerClient") BlobContainerClient reportsContainerClient, + BlobStorageProperties properties) { + + super(blobServiceClient, reportsContainerClient, properties.getInvoiceTokenDurationSeconds()); + } +} diff --git a/src/main/java/it/gov/pagopa/idpay/transactions/utils/ExceptionConstants.java b/src/main/java/it/gov/pagopa/idpay/transactions/utils/ExceptionConstants.java index 06dd1651..c39ccaa4 100644 --- a/src/main/java/it/gov/pagopa/idpay/transactions/utils/ExceptionConstants.java +++ b/src/main/java/it/gov/pagopa/idpay/transactions/utils/ExceptionConstants.java @@ -29,6 +29,7 @@ private ExceptionCode(){} public static final String MERCHANT_ID_OR_ORGANIZATION_ROLE_ARE_MANDATORY = "MERCHANT_ID_OR_ORGANIZATION_ROLE_ARE_MANDATORY"; public static final String MERCHANT_ID_AND_ORGANIZATION_ROLE_CANNOT_COEXIST = "MERCHANT_ID_AND_ORGANIZATION_ROLE_CANNOT_COEXIST"; public static final String INVALID_ORGANIZATION_ROLE = "INVALID_ORGANIZATION_ROLE"; + public static final String REPORT_MISSING_FILENAME = "REPORT_MISSING_FILENAME"; public static final String ROLE_NOT_ALLOWED_FOR_L1_PROMOTION = "ROLE_NOT_ALLOWED_FOR_L1_PROMOTION"; public static final String ROLE_NOT_ALLOWED_FOR_L2_PROMOTION = "ROLE_NOT_ALLOWED_FOR_L2_PROMOTION"; @@ -39,6 +40,7 @@ private ExceptionCode(){} public static final String REWARD_BATCH_PREVIOUS_NOT_SENT = "REWARD_BATCH_PREVIOUS_NOT_SENT"; public static final String INVALID_CHECKS_ERROR = "INVALID_CHECKS_ERROR"; public static final String REPORT_NOT_FOUND = "REPORT_NOT_FOUND"; + public static final String REPORT_NOT_GENERATED = "REPORT_NOT_GENERATED"; public static final String MERCHANT_NOT_FOUND = "MERCHANT_NOT_FOUND"; } @@ -81,5 +83,7 @@ private ExceptionMessage(){} public static final String ERROR_MESSAGE_INVALID_CHECKS_ERROR = "At least one checksError field must be true"; public static final String ERROR_MESSAGE_REPORT_NOT_FOUND = "Report %s not found for initiative %s "; public static final String ERROR_MESSAGE_MERCHANT_NOT_FOUND = "Merchant %s not found for initiative %s "; + public static final String ERROR_MESSAGE_REPORT_MISSING_FILENAME = "The report %s does not have an associated file name and cannot be downloaded"; + public static final String ERROR_MESSAGE_REPORT_NOT_GENERATED = "The report %s is not generated yet and cannot be downloaded"; } } diff --git a/src/test/java/it/gov/pagopa/idpay/transactions/controller/ReportControllerImplTest.java b/src/test/java/it/gov/pagopa/idpay/transactions/controller/ReportControllerImplTest.java index 7fcb5bd9..9e0b5941 100644 --- a/src/test/java/it/gov/pagopa/idpay/transactions/controller/ReportControllerImplTest.java +++ b/src/test/java/it/gov/pagopa/idpay/transactions/controller/ReportControllerImplTest.java @@ -1,10 +1,7 @@ package it.gov.pagopa.idpay.transactions.controller; import it.gov.pagopa.common.web.exception.ClientExceptionWithBody; -import it.gov.pagopa.idpay.transactions.dto.PatchReportRequest; -import it.gov.pagopa.idpay.transactions.dto.ReportDTO; -import it.gov.pagopa.idpay.transactions.dto.ReportListDTO; -import it.gov.pagopa.idpay.transactions.dto.ReportRequest; +import it.gov.pagopa.idpay.transactions.dto.*; import it.gov.pagopa.idpay.transactions.dto.mapper.ReportMapper; import it.gov.pagopa.idpay.transactions.enums.ReportType; import it.gov.pagopa.idpay.transactions.model.Report; @@ -285,6 +282,110 @@ void patchReport_NotFound() { .patchReport(eq(INITIATIVE_ID), eq("missingReport"), any()); } + @Test + void downloadTransactionsReport_WithMerchantId_Success() { + + DownloadReportResponseDTO responseDTO = DownloadReportResponseDTO.builder() + .reportUrl("http://localhost/report.csv?sasToken") + .build(); + + when(reportService.downloadTransactionsReport( + eq(MERCHANT_ID), + isNull(), + eq(INITIATIVE_ID), + eq("report123") + )).thenReturn(Mono.just(responseDTO)); + + webClient.get() + .uri("/idpay/merchant/portal/initiatives/{initiativeId}/reports/{reportId}/download", + INITIATIVE_ID, "report123") + .header("x-merchant-id", MERCHANT_ID) + .exchange() + .expectStatus().isOk() + .expectBody(DownloadReportResponseDTO.class) + .value(response -> { + assertNotNull(response); + assertEquals("http://localhost/report.csv?sasToken", response.getReportUrl()); + }); + + verify(reportService, times(1)) + .downloadTransactionsReport(eq(MERCHANT_ID), isNull(), eq(INITIATIVE_ID), eq("report123")); + } + + @Test + void downloadTransactionsReport_WithOrganizationRole_Success() { + + DownloadReportResponseDTO responseDTO = DownloadReportResponseDTO.builder() + .reportUrl("http://localhost/report.csv?sasToken") + .build(); + + when(reportService.downloadTransactionsReport( + isNull(), + eq("ADMIN"), + eq(INITIATIVE_ID), + eq("report123") + )).thenReturn(Mono.just(responseDTO)); + + webClient.get() + .uri("/idpay/merchant/portal/initiatives/{initiativeId}/reports/{reportId}/download", + INITIATIVE_ID, "report123") + .header("x-organization-role", "ADMIN") + .exchange() + .expectStatus().isOk() + .expectBody(DownloadReportResponseDTO.class) + .value(response -> { + assertNotNull(response); + assertEquals("http://localhost/report.csv?sasToken", response.getReportUrl()); + }); + verify(reportService, times(1)) + .downloadTransactionsReport(isNull(), eq("ADMIN"), eq(INITIATIVE_ID), eq("report123")); + } + + @Test + void downloadTransactionsReport_NotFound() { + + when(reportService.downloadTransactionsReport( + eq(MERCHANT_ID), + isNull(), + eq(INITIATIVE_ID), + eq("missingReport") + )).thenReturn(Mono.error(new ClientExceptionWithBody( + HttpStatus.NOT_FOUND, + REPORT_NOT_FOUND, + ERROR_MESSAGE_REPORT_NOT_FOUND.formatted("missingReport", INITIATIVE_ID) + ))); + + webClient.get() + .uri("/idpay/merchant/portal/initiatives/{initiativeId}/reports/{reportId}/download", + INITIATIVE_ID, "missingReport") + .header("x-merchant-id", MERCHANT_ID) + .exchange() + .expectStatus().isNotFound(); + + verify(reportService, times(1)) + .downloadTransactionsReport(eq(MERCHANT_ID), isNull(), eq(INITIATIVE_ID), eq("missingReport")); + } + + @Test + void downloadTransactionsReport_ServiceFails_InternalServerError() { + + when(reportService.downloadTransactionsReport( + eq(MERCHANT_ID), + isNull(), + eq(INITIATIVE_ID), + eq("report123") + )).thenReturn(Mono.error(new RuntimeException("Service failure"))); + + webClient.get() + .uri("/idpay/merchant/portal/initiatives/{initiativeId}/reports/{reportId}/download", + INITIATIVE_ID, "report123") + .header("x-merchant-id", MERCHANT_ID) + .exchange() + .expectStatus().is5xxServerError(); + + verify(reportService, times(1)) + .downloadTransactionsReport(eq(MERCHANT_ID), isNull(), eq(INITIATIVE_ID), eq("report123")); + } } diff --git a/src/test/java/it/gov/pagopa/idpay/transactions/service/ReportServiceImplTest.java b/src/test/java/it/gov/pagopa/idpay/transactions/service/ReportServiceImplTest.java index 60ff66d7..146a83a0 100644 --- a/src/test/java/it/gov/pagopa/idpay/transactions/service/ReportServiceImplTest.java +++ b/src/test/java/it/gov/pagopa/idpay/transactions/service/ReportServiceImplTest.java @@ -12,9 +12,12 @@ import it.gov.pagopa.idpay.transactions.enums.RewardBatchAssignee; import it.gov.pagopa.idpay.transactions.model.Report; import it.gov.pagopa.idpay.transactions.repository.ReportRepository; +import it.gov.pagopa.idpay.transactions.storage.ReportBlobService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockedStatic; @@ -27,11 +30,13 @@ import java.time.LocalDateTime; -import static it.gov.pagopa.idpay.transactions.utils.ExceptionConstants.ExceptionCode.REPORT_NOT_FOUND; +import static it.gov.pagopa.idpay.transactions.service.ReportServiceImpl.ALLOWED_ROLES; +import static it.gov.pagopa.idpay.transactions.utils.ExceptionConstants.ExceptionCode.*; import static it.gov.pagopa.idpay.transactions.utils.ExceptionConstants.ExceptionMessage.ERROR_MESSAGE_REPORT_NOT_FOUND; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; +import static org.springframework.http.HttpStatus.BAD_REQUEST; @ExtendWith(MockitoExtension.class) class ReportServiceImplTest { @@ -47,13 +52,17 @@ class ReportServiceImplTest { private ReportServiceImpl service; + @Mock + private ReportBlobService reportBlobService; + + private static final String MERCHANT_ID = "M1"; private static final String INITIATIVE_ID = "INIT1"; private static final String ORGANIZATION_ROLE = "operator1"; @BeforeEach void setup() { - service = new ReportServiceImpl(reportRepository, merchantRestClient, reportMapper); + service = new ReportServiceImpl(reportRepository, merchantRestClient, reportMapper, reportBlobService); } @Test @@ -488,4 +497,271 @@ void patchReport_notFound_throwsException() { verify(reportMapper, never()).toDTO(any()); } + @Test + void downloadTransactionsReport_success() { + + String reportId = "R1"; + String fileName = "Report_01012026120000"; + String expectedUrl = "https://signed-url"; + + Report report = Report.builder() + .id(reportId) + .initiativeId(INITIATIVE_ID) + .merchantId(MERCHANT_ID) + .reportStatus(ReportStatus.GENERATED) + .fileName(fileName) + .build(); + + when(reportRepository.findByIdAndInitiativeIdAndMerchantId( + reportId, INITIATIVE_ID, MERCHANT_ID)) + .thenReturn(Mono.just(report)); + + when(reportBlobService.getFileSignedUrl(anyString())) + .thenReturn(expectedUrl); + + StepVerifier.create( + service.downloadTransactionsReport( + MERCHANT_ID, + null, + INITIATIVE_ID, + reportId + ) + ) + .assertNext(response -> { + assertEquals(expectedUrl, response.getReportUrl()); + }) + .verifyComplete(); + + verify(reportBlobService).getFileSignedUrl(contains(fileName)); + } + + @Test + void downloadTransactionsReport_notGenerated_throwsException() { + + String reportId = "R1"; + + Report report = Report.builder() + .id(reportId) + .initiativeId(INITIATIVE_ID) + .merchantId(MERCHANT_ID) + .reportStatus(ReportStatus.INSERTED) + .fileName("file.csv") + .build(); + + when(reportRepository.findByIdAndInitiativeIdAndMerchantId( + reportId, INITIATIVE_ID, MERCHANT_ID)) + .thenReturn(Mono.just(report)); + + StepVerifier.create( + service.downloadTransactionsReport( + MERCHANT_ID, + null, + INITIATIVE_ID, + reportId + ) + ) + .expectErrorSatisfies(error -> { + assertInstanceOf(ClientExceptionWithBody.class, error); + ClientExceptionWithBody ex = (ClientExceptionWithBody) error; + assertEquals(400, ex.getHttpStatus().value()); + assertEquals("REPORT_NOT_GENERATED", ex.getCode()); + assertEquals( + "The report R1 is not generated yet and cannot be downloaded", + ex.getMessage() + ); + }) + .verify(); + } + + @Test + void downloadTransactionsReport_missingFilename_throwsException() { + + String reportId = "R1"; + + Report report = Report.builder() + .id(reportId) + .initiativeId(INITIATIVE_ID) + .merchantId(MERCHANT_ID) + .reportStatus(ReportStatus.GENERATED) + .fileName(null) + .build(); + + when(reportRepository.findByIdAndInitiativeIdAndMerchantId( + reportId, INITIATIVE_ID, MERCHANT_ID)) + .thenReturn(Mono.just(report)); + + StepVerifier.create( + service.downloadTransactionsReport( + MERCHANT_ID, + null, + INITIATIVE_ID, + reportId + ) + ) + .expectErrorSatisfies(error -> { + assertInstanceOf(ClientExceptionWithBody.class, error); + ClientExceptionWithBody ex = (ClientExceptionWithBody) error; + assertEquals(500, ex.getHttpStatus().value()); + }) + .verify(); + } + + @Test + void downloadTransactionsReport_notFound() { + + when(reportRepository.findByIdAndInitiativeIdAndMerchantId( + anyString(), anyString(), anyString())) + .thenReturn(Mono.empty()); + + StepVerifier.create( + service.downloadTransactionsReport( + MERCHANT_ID, + null, + INITIATIVE_ID, + "missing" + ) + ) + .expectErrorSatisfies(error -> { + assertInstanceOf(ClientExceptionWithBody.class, error); + ClientExceptionWithBody ex = (ClientExceptionWithBody) error; + assertEquals(404, ex.getHttpStatus().value()); + }) + .verify(); + } + + @Test + void downloadTransactionsReport_bothMerchantAndRoleMissing_returnsBadRequest() { + + StepVerifier.create( + service.downloadTransactionsReport( + null, + null, + INITIATIVE_ID, + "R1" + ) + ) + .expectErrorSatisfies(error -> { + assertInstanceOf(ClientExceptionWithBody.class, error); + ClientExceptionWithBody ex = (ClientExceptionWithBody) error; + + assertEquals(400, ex.getHttpStatus().value()); + assertEquals(MERCHANT_ID_OR_ORGANIZATION_ROLE_ARE_MANDATORY, ex.getCode()); + }) + .verify(); + + verifyNoInteractions(reportRepository); + } + + @Test + void downloadTransactionsReport_merchantAndRoleBothPresent_returnsBadRequest() { + + StepVerifier.create( + service.downloadTransactionsReport( + MERCHANT_ID, + "ADMIN", + INITIATIVE_ID, + "R1" + ) + ) + .expectErrorSatisfies(error -> { + assertInstanceOf(ClientExceptionWithBody.class, error); + ClientExceptionWithBody ex = (ClientExceptionWithBody) error; + + assertEquals(400, ex.getHttpStatus().value()); + assertEquals(MERCHANT_ID_AND_ORGANIZATION_ROLE_CANNOT_COEXIST, ex.getCode()); + }) + .verify(); + + verifyNoInteractions(reportRepository); + } + + + @Test + void downloadTransactionsReport_invalidOrganizationRole_returnsBadRequest() { + + StepVerifier.create( + service.downloadTransactionsReport( + null, + "INVALID_ROLE", + INITIATIVE_ID, + "R1" + ) + ) + .expectErrorSatisfies(error -> { + assertInstanceOf(ClientExceptionWithBody.class, error); + ClientExceptionWithBody ex = (ClientExceptionWithBody) error; + + assertEquals(400, ex.getHttpStatus().value()); + assertEquals(INVALID_ORGANIZATION_ROLE, ex.getCode()); + }) + .verify(); + + verifyNoInteractions(reportRepository); + } + + @Test + void downloadTransactionsReport_withOrganizationRole_callsCorrectRepositoryMethod() { + + Report report = Report.builder() + .id("R1") + .initiativeId(INITIATIVE_ID) + .merchantId("M1") + .reportStatus(ReportStatus.GENERATED) + .fileName("file.csv") + .build(); + + when(reportRepository.findByIdAndInitiativeId("R1", INITIATIVE_ID)) + .thenReturn(Mono.just(report)); + + when(reportBlobService.getFileSignedUrl(anyString())) + .thenReturn("signed-url"); + + StepVerifier.create( + service.downloadTransactionsReport( + null, + ALLOWED_ROLES.get(0), + INITIATIVE_ID, + "R1" + ) + ) + .expectNextCount(1) + .verifyComplete(); + + verify(reportRepository).findByIdAndInitiativeId("R1", INITIATIVE_ID); + verify(reportRepository, never()) + .findByIdAndInitiativeIdAndMerchantId(any(), any(), any()); + } + + @ParameterizedTest + @CsvSource({ + ",", + "'',", + ",'',", + "'',''" + }) + void downloadTransactionsReport_bothMerchantAndRoleMissing_returnsBadRequest( + String merchantId, + String organizationRole + ) { + + StepVerifier.create( + service.downloadTransactionsReport( + merchantId, + organizationRole, + INITIATIVE_ID, + "R1" + ) + ) + .expectErrorSatisfies(error -> { + assertInstanceOf(ClientExceptionWithBody.class, error); + ClientExceptionWithBody ex = (ClientExceptionWithBody) error; + + assertEquals(BAD_REQUEST, ex.getHttpStatus()); + assertEquals(MERCHANT_ID_OR_ORGANIZATION_ROLE_ARE_MANDATORY, ex.getCode()); + }) + .verify(); + + verifyNoInteractions(reportRepository); + } + } diff --git a/src/test/java/it/gov/pagopa/idpay/transactions/storage/BlobStorageClientConfigTest.java b/src/test/java/it/gov/pagopa/idpay/transactions/storage/BlobStorageClientConfigTest.java index 52bbd446..f5e0f22e 100644 --- a/src/test/java/it/gov/pagopa/idpay/transactions/storage/BlobStorageClientConfigTest.java +++ b/src/test/java/it/gov/pagopa/idpay/transactions/storage/BlobStorageClientConfigTest.java @@ -35,4 +35,19 @@ void testBlobContainerClient() { assertNotNull(containerClient); assert(containerClient.getBlobContainerName().equals("containerreference")); } + + @Test + void testReportsContainerClient() { + BlobServiceClient serviceClient = blobStorageClientConfig.blobServiceClient(); + + BlobStorageProperties properties = new BlobStorageProperties(); + properties.setReportsContainerReference("reportsContainer"); + + BlobStorageClientConfig config = new BlobStorageClientConfig(properties); + + BlobContainerClient reportsClient = config.reportsContainerClient(serviceClient); + assertNotNull(reportsClient); + assert(reportsClient.getBlobContainerName().equals("reportsContainer")); + } + } diff --git a/src/test/java/it/gov/pagopa/idpay/transactions/storage/ReportBlobServiceImplTest.java b/src/test/java/it/gov/pagopa/idpay/transactions/storage/ReportBlobServiceImplTest.java new file mode 100644 index 00000000..ac446962 --- /dev/null +++ b/src/test/java/it/gov/pagopa/idpay/transactions/storage/ReportBlobServiceImplTest.java @@ -0,0 +1,117 @@ +package it.gov.pagopa.idpay.transactions.storage; + +import com.azure.core.http.rest.Response; +import com.azure.storage.blob.BlobClient; +import com.azure.storage.blob.BlobContainerClient; +import com.azure.storage.blob.BlobServiceClient; +import com.azure.storage.blob.models.BlobStorageException; +import com.azure.storage.blob.models.BlockBlobItem; +import com.azure.storage.blob.options.BlobParallelUploadOptions; +import it.gov.pagopa.common.web.exception.ClientException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ReportBlobServiceImplTest { + + @Mock + private BlobClient blobClientMock; + + @Mock + private BlobServiceClient blobServiceClient; + + @Mock + private BlobContainerClient reportsContainerClient; + + @Mock + private BlobStorageProperties propertiesMock; + + private ReportBlobServiceImpl reportService; + + @BeforeEach + void init() { + when(propertiesMock.getInvoiceTokenDurationSeconds()).thenReturn(60); + + lenient().when(reportsContainerClient.getBlobClient(anyString())) + .thenReturn(blobClientMock); + + reportService = new ReportBlobServiceImpl( + blobServiceClient, + reportsContainerClient, + propertiesMock + ); + } + + @Test + void getFileSignedUrlShouldReturnOK() { + when(blobClientMock.getBlobUrl()).thenReturn("http://localhost:8080"); + when(blobClientMock.generateUserDelegationSas(any(), any())) + .thenReturn("token"); + + String url = reportService.getFileSignedUrl("fileA.csv"); + + assertNotNull(url); + assertEquals("http://localhost:8080?token", url); + } + + @Test + void getFileSignedUrlShouldThrowException() { + when(blobClientMock.generateUserDelegationSas(any(), any())) + .thenThrow(new BlobStorageException("sas error", null, null)); + + assertThrows(ClientException.class, + () -> reportService.getFileSignedUrl("fileA.csv")); + } + + @Test + void uploadShouldReturnOK() { + InputStream input = new ByteArrayInputStream("report content".getBytes()); + String destination = "path/report.csv"; + + Response mockResponse = mock(Response.class); + + when(blobClientMock.uploadWithResponse( + any(BlobParallelUploadOptions.class), + any(), + any() + )).thenReturn(mockResponse); + + Response result = + reportService.upload(input, destination, "text/csv"); + + assertNotNull(result); + + verify(reportsContainerClient).getBlobClient(destination); + verify(blobClientMock).uploadWithResponse( + any(BlobParallelUploadOptions.class), + any(), + any() + ); + } + + @Test + void uploadShouldThrowException() { + InputStream input = new ByteArrayInputStream("report content".getBytes()); + String destination = "path/report.csv"; + + when(blobClientMock.uploadWithResponse( + any(BlobParallelUploadOptions.class), + any(), + any() + )).thenThrow(new RuntimeException("upload error")); + + assertThrows(RuntimeException.class, + () -> reportService.upload(input, destination, "text/csv")); + } +} From 1a230894834986ae3073846346c85bbd28913e72 Mon Sep 17 00:00:00 2001 From: v1tt0ri0Alt Date: Fri, 13 Feb 2026 17:49:02 +0100 Subject: [PATCH 2/2] fix merge --- .../idpay/transactions/controller/ReportController.java | 5 +---- .../idpay/transactions/controller/ReportControllerImpl.java | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/main/java/it/gov/pagopa/idpay/transactions/controller/ReportController.java b/src/main/java/it/gov/pagopa/idpay/transactions/controller/ReportController.java index 3b7087bc..bd19fad4 100644 --- a/src/main/java/it/gov/pagopa/idpay/transactions/controller/ReportController.java +++ b/src/main/java/it/gov/pagopa/idpay/transactions/controller/ReportController.java @@ -1,9 +1,6 @@ package it.gov.pagopa.idpay.transactions.controller; -import it.gov.pagopa.idpay.transactions.dto.PatchReportRequest; -import it.gov.pagopa.idpay.transactions.dto.ReportDTO; -import it.gov.pagopa.idpay.transactions.dto.ReportListDTO; -import it.gov.pagopa.idpay.transactions.dto.ReportRequest; +import it.gov.pagopa.idpay.transactions.dto.*; import it.gov.pagopa.idpay.transactions.dto.report.Report2RunDto; import it.gov.pagopa.idpay.transactions.dto.report.ReportGenerateForce; import jakarta.validation.Valid; diff --git a/src/main/java/it/gov/pagopa/idpay/transactions/controller/ReportControllerImpl.java b/src/main/java/it/gov/pagopa/idpay/transactions/controller/ReportControllerImpl.java index 361a0bd6..b53d0980 100644 --- a/src/main/java/it/gov/pagopa/idpay/transactions/controller/ReportControllerImpl.java +++ b/src/main/java/it/gov/pagopa/idpay/transactions/controller/ReportControllerImpl.java @@ -1,9 +1,6 @@ package it.gov.pagopa.idpay.transactions.controller; -import it.gov.pagopa.idpay.transactions.dto.PatchReportRequest; -import it.gov.pagopa.idpay.transactions.dto.ReportDTO; -import it.gov.pagopa.idpay.transactions.dto.ReportListDTO; -import it.gov.pagopa.idpay.transactions.dto.ReportRequest; +import it.gov.pagopa.idpay.transactions.dto.*; import it.gov.pagopa.idpay.transactions.dto.report.Report2RunDto; import it.gov.pagopa.idpay.transactions.dto.report.ReportGenerateForce; import it.gov.pagopa.idpay.transactions.service.ReportService;