Repository: pagopa/pagopa-ecommerce-reporting-functions
Pull request #12: feat: split large slack messages in 50 blocks each (API hard limit)
Author: login: sfarralorenzo
Created: 2025-04-25T08:13:20Z | Merged: 2025-04-27T16:40:04Z

<!--- Please always add a PR description as if nobody knows anything about the context these changes come from. -->
<!--- Even if we are all from our internal team, we may not be on the same page. -->
<!--- Write this PR as you were contributing to a public OSS project, where nobody knows you and you have to earn their trust. -->
<!--- This will improve our projects in the long run! Thanks. -->

This PR splits blocks in chunks, maximum 50 for each slack message(hard limit).
It also sleeps for 1 second between each slack api call (also an hard limit).

#### List of Changes
Support for more than 50 blocks per run

<!--- Describe your changes in detail -->

#### Motivation and Context
Slack has hard limits on the max blocks number per each message.
This is addressed in this PR splitting the blocks in different messages.

<!--- Why is this change required? What problem does it solve? -->

#### How Has This Been Tested?
Local

<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, tests ran to see how -->
<!--- your change affects other areas of the code, etc. -->

#### Screenshots (if appropriate):

#### Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)

#### Checklist:

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [ ] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly.


Diff:
diff --git a/README.md b/README.md
index 01b81c6..87a7675 100644
--- a/README.md
+++ b/README.md
@@ -14,8 +14,10 @@ These are all environment variables needed by the application:
 | ECOMMERCE_HELPDESK_SERVICE_READ_TIMEOUT       |     | Timeout for requests towards eCommerce Helpdesk service                 | number |         |
 | ECOMMERCE_HELPDESK_SERVICE_CONNECTION_TIMEOUT |     | Timeout for establishing connections towards eCommerce Helpdesk service | number |         |
 | ECOMMERCE_HELPDESK_SERVICE_API_KEY            |     | Helpdesk methods API key                                                | string |         |
+| REPORT_START_DATE                             |     | Force report start date. Format: d-m-y. Ex.: 23-04-2025.                | string |         |
+| REPORT_END_DATE                               |     | Force report end date. Format: d-m-y. Ex.: 23-04-2025.                  | string |         |
 
-An example configuration of these environment variables is in the `.env.example` file.
+An example configuration of these environment variables is in the `local.settings.json.example` file.
 
 ## Installation
 
diff --git a/helm/Chart.yaml b/helm/Chart.yaml
index 4efd984..b2da0d3 100644
--- a/helm/Chart.yaml
+++ b/helm/Chart.yaml
@@ -2,8 +2,8 @@ apiVersion: v2
 name: pagopa-ecommerce-reporting-functions
 description: Microservice that handles reporting for ecommerce
 type: application
-version: 1.0.1
-appVersion: 1.0.1
+version: 1.3.0
+appVersion: 1.3.0
 dependencies:
   - name: microservice-chart
     version: 7.4.0
diff --git a/helm/values-dev.yaml b/helm/values-dev.yaml
index 5356424..671a95a 100644
--- a/helm/values-dev.yaml
+++ b/helm/values-dev.yaml
@@ -4,7 +4,7 @@ microservice-chart:
   fullnameOverride: "pagopa-ecommerce-reporting-functions"
   image:
     repository: pagopadcommonacr.azurecr.io/pagopaecommercereportingfunctions
-    tag: "1.0.1"
+    tag: "1.3.0"
     pullPolicy: Always
   livenessProbe:
     httpGet:
@@ -83,6 +83,8 @@ microservice-chart:
     NCRON_SCHEDULE_CHECKOUT_CART: "0 21 * * * *"
     NCRON_SCHEDULE_WISP_REDIRECT: "0 31 * * * *"
     NCRON_SCHEDULE_SLACK_REPORTING: "0 0 9 * * Mon"
+    REPORT_START_DATE: ""
+    REPORT_END_DATE: ""
   envSecret:
     OTEL_EXPORTER_OTLP_HEADERS: elastic-otel-token-header
     ECOMMERCE_SLACK_REPORTING_WEBHOOK_ENDPOINT: ecommerce-reporting-webhook-url-slack
diff --git a/helm/values-prod.yaml b/helm/values-prod.yaml
index 7533304..cb1741a 100644
--- a/helm/values-prod.yaml
+++ b/helm/values-prod.yaml
@@ -4,7 +4,7 @@ microservice-chart:
   fullnameOverride: "pagopa-ecommerce-reporting-functions"
   image:
     repository: pagopapcommonacr.azurecr.io/pagopaecommercereportingfunctions
-    tag: "1.0.1"
+    tag: "1.3.0"
     pullPolicy: Always
   livenessProbe:
     httpGet:
@@ -82,7 +82,9 @@ microservice-chart:
     NCRON_SCHEDULE_IO: "0 11 * * * *"
     NCRON_SCHEDULE_CHECKOUT_CART: "0 21 * * * *"
     NCRON_SCHEDULE_WISP_REDIRECT: "0 31 * * * *"
-    NCRON_SCHEDULE_SLACK_REPORTING: "0 0 9 * * Mon"
+    NCRON_SCHEDULE_SLACK_REPORTING: "0 20 9 * * Mon"
+    REPORT_START_DATE: ""
+    REPORT_END_DATE: ""
   envSecret:
     OTEL_EXPORTER_OTLP_HEADERS: elastic-otel-token-header
     ECOMMERCE_SLACK_REPORTING_WEBHOOK_ENDPOINT: ecommerce-reporting-webhook-url-slack
diff --git a/helm/values-uat.yaml b/helm/values-uat.yaml
index df83afa..5fd6db6 100644
--- a/helm/values-uat.yaml
+++ b/helm/values-uat.yaml
@@ -4,7 +4,7 @@ microservice-chart:
   fullnameOverride: "pagopa-ecommerce-reporting-functions"
   image:
     repository: pagopaucommonacr.azurecr.io/pagopaecommercereportingfunctions
-    tag: "1.0.1"
+    tag: "1.3.0"
     pullPolicy: Always
   livenessProbe:
     httpGet:
@@ -83,6 +83,8 @@ microservice-chart:
     NCRON_SCHEDULE_CHECKOUT_CART: "0 21 * * * *"
     NCRON_SCHEDULE_WISP_REDIRECT: "0 31 * * * *"
     NCRON_SCHEDULE_SLACK_REPORTING: "0 0 9 * * Mon"
+    REPORT_START_DATE: ""
+    REPORT_END_DATE: ""
   envSecret:
     OTEL_EXPORTER_OTLP_HEADERS: elastic-otel-token-header
     ECOMMERCE_SLACK_REPORTING_WEBHOOK_ENDPOINT: ecommerce-reporting-webhook-url-slack
diff --git a/pom.xml b/pom.xml
index 4bdf000..7428629 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
     <modelVersion>4.0.0</modelVersion>
     <groupId>it.pagopa</groupId>
     <artifactId>pagopa-ecommerce-reporting-functions</artifactId>
-    <version>1.0.1</version>
+    <version>1.3.0</version>
     <packaging>jar</packaging>
     <name>pagopa-ecommerce-reporting-functionse</name>
     <description>pagopa-ecommerce-reporting-functions</description>
diff --git a/src/main/java/it/pagopa/ecommerce/reporting/clients/SlackWebhookClient.java b/src/main/java/it/pagopa/ecommerce/reporting/clients/SlackWebhookClient.java
index 21ba9bf..4f6042e 100644
--- a/src/main/java/it/pagopa/ecommerce/reporting/clients/SlackWebhookClient.java
+++ b/src/main/java/it/pagopa/ecommerce/reporting/clients/SlackWebhookClient.java
@@ -72,7 +72,7 @@ public void postRawJsonToWebhook(String jsonPayload) {
 
                 // Check for Slack error responses
                 if (statusCode != 200) {
-                    logger.info("Error response from Slack: " + responseBody);
+                    logger.info("Error response from Slack: " + responseBody + ", " + jsonPayload);
                 }
             }
         } catch (Exception e) {
diff --git a/src/main/java/it/pagopa/ecommerce/reporting/functions/SlackReportingTimerTriggered.java b/src/main/java/it/pagopa/ecommerce/reporting/functions/SlackReportingTimerTriggered.java
index 25826f1..98c84d1 100644
--- a/src/main/java/it/pagopa/ecommerce/reporting/functions/SlackReportingTimerTriggered.java
+++ b/src/main/java/it/pagopa/ecommerce/reporting/functions/SlackReportingTimerTriggered.java
@@ -1,6 +1,7 @@
 package it.pagopa.ecommerce.reporting.functions;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
 import com.microsoft.azure.functions.*;
 import com.microsoft.azure.functions.annotation.FunctionName;
 import com.microsoft.azure.functions.annotation.TimerTrigger;
@@ -9,10 +10,13 @@
 import it.pagopa.ecommerce.reporting.utils.AggregatedStatusGroup;
 import it.pagopa.ecommerce.reporting.utils.SlackDateRangeReportMessageUtils;
 
-import java.time.DayOfWeek;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
+import java.time.*;
 import java.util.List;
+import java.util.TimerTask;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.logging.Logger;
 
 /**
@@ -35,25 +39,73 @@ public void run(
         logger.info("Java Timer trigger SlackReportingTimerTriggered executed at: " + LocalDateTime.now());
 
         String endpoint = getEnvVariable("ECOMMERCE_SLACK_REPORTING_WEBHOOK_ENDPOINT");
+        String reportStartDate = getEnvVariable("REPORT_START_DATE");
+        String reportEndDate = getEnvVariable("REPORT_END_DATE");
 
         SlackWebhookClient slackWebhookClient = createSlackWebhookClient(endpoint);
         LocalDate today = getCurrentDate();
-        LocalDate lastMonday = today.minusWeeks(1).with(DayOfWeek.MONDAY);
-        LocalDate lastSunday = lastMonday.with(DayOfWeek.SUNDAY);
+
+        LocalDate startDate = today.minusWeeks(1).with(DayOfWeek.MONDAY);
+        LocalDate endDate = startDate.with(DayOfWeek.SUNDAY);
+
+        if (reportStartDate != null && reportEndDate != null) {
+            startDate = getDateFromString(reportStartDate, startDate);
+            endDate = getDateFromString(reportEndDate, endDate);
+        }
 
         TransactionStatusAggregationService transactionStatusAggregationService = createAggregationService();
         List<AggregatedStatusGroup> aggregatedStatuses = transactionStatusAggregationService
-                .aggregateStatusCountByDateRange(lastMonday, lastSunday, logger);
+                .aggregateStatusCountByDateRange(startDate, endDate, logger);
 
         logger.info(
-                "Start date: " + lastMonday + " to date: " + lastSunday + ", results: " + aggregatedStatuses.size()
+                "Start date: " + startDate + " to date: " + endDate + ", results: " + aggregatedStatuses.size()
         );
 
-        // Create the report message
-        String report = SlackDateRangeReportMessageUtils
-                .createAggregatedWeeklyReport(aggregatedStatuses, lastMonday, lastSunday, logger);
-        // Send it to Slack
-        slackWebhookClient.postMessageToWebhook(report);
+        // Create the report messages
+        String[] reportMessages = SlackDateRangeReportMessageUtils
+                .createAggregatedWeeklyReport(aggregatedStatuses, startDate, endDate, logger);
+
+        // Send each message to Slack
+        logger.info("Sending " + reportMessages.length + " messages to Slack");
+        ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
+        AtomicInteger index = new AtomicInteger(0);
+        logger.info("Start read and write");
+        ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
+        for (String reportMessage : reportMessages) {
+            index.getAndIncrement();
+            TimerTask task = new TimerTask() {
+
+                @Override
+                public void run() {
+                    logger.info("Sending message " + (index.get()) + " of " + reportMessages.length);
+                    slackWebhookClient.postMessageToWebhook(reportMessage);
+                }
+
+            };
+            scheduledExecutorService.schedule(task, index.get(), TimeUnit.SECONDS);
+
+        }
+
+        logger.info("All messages sent successfully");
+    }
+
+    protected LocalDate getDateFromString(
+                                          String dateFormat,
+                                          LocalDate defaultDate
+    ) {
+        try {
+            String[] dateComponents = dateFormat.split("-");
+            if (dateComponents.length == 3) {
+                int day = Integer.parseInt(dateComponents[0]);
+                int month = Integer.parseInt(dateComponents[1]);
+                int year = Integer.parseInt(dateComponents[2]);
+                return LocalDate.now().withYear(year).withMonth(month).withDayOfMonth(day);
+            }
+        } catch (NumberFormatException e) {
+            return defaultDate;
+        }
+
+        return defaultDate;
     }
 
     /**
diff --git a/src/main/java/it/pagopa/ecommerce/reporting/services/TransactionStatusAggregationService.java b/src/main/java/it/pagopa/ecommerce/reporting/services/TransactionStatusAggregationService.java
index b404b4f..82229d5 100644
--- a/src/main/java/it/pagopa/ecommerce/reporting/services/TransactionStatusAggregationService.java
+++ b/src/main/java/it/pagopa/ecommerce/reporting/services/TransactionStatusAggregationService.java
@@ -116,6 +116,11 @@ public List<AggregatedStatusGroup> aggregateStatusCountByDateRange(
 
         List<AggregatedStatusGroup> aggregated = new ArrayList<>(aggregatedMap.values());
 
+        // Filter out statuses with count 0 from each AggregatedStatusGroup
+        for (AggregatedStatusGroup group : aggregated) {
+            group.filterZeroCountStatuses();
+        }
+
         logger.info("[aggregateStatusCountByDateRange] Aggregation transaction status completed.");
         return aggregated;
     }
diff --git a/src/main/java/it/pagopa/ecommerce/reporting/utils/AggregatedStatusGroup.java b/src/main/java/it/pagopa/ecommerce/reporting/utils/AggregatedStatusGroup.java
index f64f535..c57412c 100644
--- a/src/main/java/it/pagopa/ecommerce/reporting/utils/AggregatedStatusGroup.java
+++ b/src/main/java/it/pagopa/ecommerce/reporting/utils/AggregatedStatusGroup.java
@@ -47,4 +47,16 @@ public String toString() {
                 statusCounts
         );
     }
+
+    public void filterZeroCountStatuses() {
+        Map<String, Integer> filteredCounts = new HashMap<>();
+
+        for (Map.Entry<String, Integer> entry : statusCounts.entrySet()) {
+            if (entry.getValue() > 0) {
+                filteredCounts.put(entry.getKey(), entry.getValue());
+            }
+        }
+
+        this.statusCounts = filteredCounts;
+    }
 }
diff --git a/src/main/java/it/pagopa/ecommerce/reporting/utils/SlackDateRangeReportMessageUtils.java b/src/main/java/it/pagopa/ecommerce/reporting/utils/SlackDateRangeReportMessageUtils.java
index 752788d..7c0b99a 100644
--- a/src/main/java/it/pagopa/ecommerce/reporting/utils/SlackDateRangeReportMessageUtils.java
+++ b/src/main/java/it/pagopa/ecommerce/reporting/utils/SlackDateRangeReportMessageUtils.java
@@ -15,54 +15,153 @@
  *      block elements reference</a>
  */
 public class SlackDateRangeReportMessageUtils {
-
     private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
     private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("d MMMM yyyy", Locale.ITALIAN);
-
     private static final String PAGOPA_LOGO_URL = "https://developer.pagopa.it/gitbook/docs/8phwN5u2QXllSKsqBjQU/.gitbook/assets/logo_asset.png";
+    private static final int MAX_BLOCKS_PER_MESSAGE = 40; // Reduced from 49 for safety
 
     /**
-     * Creates an aggregated weekly report message for Slack.
+     * Creates an aggregated weekly report message for Slack, split into multiple
+     * messages with a maximum blocks size each.
      *
      * @param aggregatedGroups List of aggregated status groups
      * @param startDate        Report start date
      * @param endDate          Report end date
      * @param logger           Logger instance
-     * @return Formatted Slack message in JSON format
+     * @return Array of formatted Slack messages in JSON format
      * @throws JsonProcessingException If JSON conversion fails
      */
-    public static String createAggregatedWeeklyReport(
-                                                      List<AggregatedStatusGroup> aggregatedGroups,
-                                                      LocalDate startDate,
-                                                      LocalDate endDate,
-                                                      Logger logger
+    public static String[] createAggregatedWeeklyReport(
+                                                        List<AggregatedStatusGroup> aggregatedGroups,
+                                                        LocalDate startDate,
+                                                        LocalDate endDate,
+                                                        Logger logger
     ) throws JsonProcessingException {
+        if (aggregatedGroups == null || aggregatedGroups.isEmpty()) {
+            logger.info("No aggregated groups to report");
+            return new String[] {
+                    createEmptyReportMessage(startDate, endDate)
+            };
+        }
 
         List<AggregatedStatusGroup> sortedGroups = sortAggregatedGroups(aggregatedGroups);
         String formattedStartDate = formatDate(startDate);
         String formattedEndDate = formatDate(endDate);
 
-        Map<String, Object> message = new HashMap<>();
-        List<Map<String, Object>> blocks = new ArrayList<>();
+        // First message with header
+        List<Map<String, Object>> firstMessageBlocks = new ArrayList<>();
+        firstMessageBlocks.add(createHeaderBlock(formattedStartDate, formattedEndDate));
+        firstMessageBlocks.add(createImageBlock());
+        firstMessageBlocks.add(createHeaderDescriptionBlock(formattedStartDate, formattedEndDate));
+        firstMessageBlocks.add(createDivider());
+
+        // Add some groups to first message if space allows
+        int groupsInFirstMessage = addGroupsToBlocks(
+                sortedGroups,
+                0,
+                MAX_BLOCKS_PER_MESSAGE - firstMessageBlocks.size(),
+                firstMessageBlocks
+        );
 
-        // Add header blocks
+        // Create first message
+        List<String> messages = new ArrayList<>();
+        Map<String, Object> firstMessage = new HashMap<>();
+        firstMessage.put("blocks", firstMessageBlocks);
+        messages.add(OBJECT_MAPPER.writeValueAsString(firstMessage));
+
+        // Create additional messages for remaining groups
+        int remainingGroups = sortedGroups.size() - groupsInFirstMessage;
+        if (remainingGroups > 0) {
+            int currentGroupIndex = groupsInFirstMessage;
+
+            while (currentGroupIndex < sortedGroups.size()) {
+                List<Map<String, Object>> messageBlocks = new ArrayList<>();
+
+                // Add groups to this message
+                int groupsAdded = addGroupsToBlocks(
+                        sortedGroups,
+                        currentGroupIndex,
+                        MAX_BLOCKS_PER_MESSAGE,
+                        messageBlocks
+                );
+
+                // Create message
+                Map<String, Object> message = new HashMap<>();
+                if (!messageBlocks.isEmpty()) {
+                    message.put("blocks", messageBlocks);
+                    messages.add(OBJECT_MAPPER.writeValueAsString(message));
+                }
+
+                currentGroupIndex += groupsAdded;
+            }
+        }
+
+        logger.info("Created " + messages.size() + " slack messages");
+        return messages.toArray(new String[0]);
+    }
+
+    /**
+     * Creates an empty report message when no data is available.
+     */
+    private static String createEmptyReportMessage(
+                                                   LocalDate startDate,
+                                                   LocalDate endDate
+    )
+            throws JsonProcessingException {
+        String formattedStartDate = formatDate(startDate);
+        String formattedEndDate = formatDate(endDate);
+
+        List<Map<String, Object>> blocks = new ArrayList<>();
         blocks.add(createHeaderBlock(formattedStartDate, formattedEndDate));
         blocks.add(createImageBlock());
-        blocks.add(createHeaderDescriptionBlock(formattedStartDate, formattedEndDate));
-        blocks.add(createDivider());
-
-        // Add sections for each aggregated group
-        for (AggregatedStatusGroup group : sortedGroups) {
-            blocks.add(createGroupHeaderSection(group));
-            blocks.add(createStatusDetailsSection(group));
-            blocks.add(createDivider());
-        }
+        blocks.add(
+                createTextBlock(
+                        "section",
+                        "mrkdwn",
+                        "Non ci sono dati da visualizzare per il periodo selezionato.",
+                        false
+                )
+        );
 
+        Map<String, Object> message = new HashMap<>();
         message.put("blocks", blocks);
-        logger.info("slack message " + blocks.size() + " blocks created successfully");
         return OBJECT_MAPPER.writeValueAsString(message);
     }
 
+    /**
+     * Adds groups to a block list and returns the number of groups added.
+     */
+    private static int addGroupsToBlocks(
+                                         List<AggregatedStatusGroup> groups,
+                                         int startIndex,
+                                         int maxBlocksAvailable,
+                                         List<Map<String, Object>> blocks
+    ) {
+
+        int blocksPerGroup = 3; // Header + Details + Divider
+        int maxGroupsToAdd = maxBlocksAvailable / blocksPerGroup;
+        int groupsToAdd = Math.min(maxGroupsToAdd, groups.size() - startIndex);
+
+        for (int i = 0; i < groupsToAdd; i++) {
+            AggregatedStatusGroup group = groups.get(startIndex + i);
+
+            // Only add groups that have status counts
+            if (!group.getStatusCounts().isEmpty()) {
+                blocks.add(createGroupHeaderSection(group));
+
+                // Only add status details if there are any
+                String statusDetails = formatStatusDetails(group.getStatusCounts());
+                if (!statusDetails.trim().isEmpty()) {
+                    blocks.add(createStatusDetailsSection(group));
+                }
+
+                blocks.add(createDivider());
+            }
+        }
+
+        return groupsToAdd;
+    }
+
     /**
      * Sorts aggregated groups by ACTIVATED count in descending order.
      *
@@ -97,11 +196,20 @@ static String formatDate(LocalDate date) {
      * @return Formatted status details string
      */
     static String formatStatusDetails(Map<String, Integer> statusCounts) {
+        if (statusCounts == null || statusCounts.isEmpty()) {
+            return "";
+        }
+
         StringBuilder statusDetails = new StringBuilder();
         List<String> sortedKeys = new ArrayList<>(statusCounts.keySet());
         Collections.sort(sortedKeys);
 
         for (String statusKey : sortedKeys) {
+            Integer count = statusCounts.get(statusKey);
+            if (count == null || count == 0) {
+                continue; // Skip zero counts
+            }
+
             SlackMessageConstants.TranslationEntry entry = SlackMessageConstants.STATUS_TRANSLATIONS.getOrDefault(
                     statusKey,
                     new SlackMessageConstants.TranslationEntry(statusKey, SlackMessageConstants.DEFAULT_STATUS_EMOJI)
@@ -112,7 +220,7 @@ static String formatStatusDetails(Map<String, Integer> statusCounts) {
                     .append(" *")
                     .append(entry.translation())
                     .append("*: ")
-                    .append(statusCounts.get(statusKey))
+                    .append(count)
                     .append("\n\n");
         }
 
@@ -126,6 +234,10 @@ static String formatStatusDetails(Map<String, Integer> statusCounts) {
      * @return Formatted payment type string
      */
     static String formatPaymentTypeCode(String paymentTypeCode) {
+        if (paymentTypeCode == null || paymentTypeCode.isEmpty()) {
+            paymentTypeCode = "GENERIC";
+        }
+
         SlackMessageConstants.TranslationEntry entry = SlackMessageConstants.PAYMENT_TYPE_CODE.getOrDefault(
                 paymentTypeCode,
                 new SlackMessageConstants.TranslationEntry(
@@ -156,7 +268,7 @@ static Map<String, Object> createHeaderDescriptionBlock(
                                                             String endDate
     ) {
         return createTextBlock(
-                "header",
+                "section",
                 "plain_text",
                 "Di seguito il report suddiviso per Client, PSP e metodo pagamento di pagamento per l'intervallo di tempo dal "
                         + startDate + " al " + endDate,
@@ -169,8 +281,6 @@ static Map<String, Object> createImageBlock() {
         imageBlock.put("type", "image");
         imageBlock.put("image_url", PAGOPA_LOGO_URL);
         imageBlock.put("alt_text", "PagoPA Logo");
-        imageBlock.put("image_width", 474);
-        imageBlock.put("image_height", 133);
         return imageBlock;
     }
 
@@ -181,19 +291,28 @@ static Map<String, Object> createDivider() {
     }
 
     static Map<String, Object> createGroupHeaderSection(AggregatedStatusGroup group) {
+        String clientId = group.getClientId() != null ? group.getClientId() : "Unknown";
+        String pspId = group.getPspId() != null ? group.getPspId() : "Unknown";
+        String paymentTypeCode = group.getPaymentTypeCode();
+
         String text = SlackMessageConstants.CHART_EMOJI + " STATISTICHE" +
-                "\n\t\t| Client *" + group.getClientId() +
-                "* con PSP *" + group.getPspId() +
-                "* e pagato con " + formatPaymentTypeCode(group.getPaymentTypeCode());
+                "\n\t\t| Client *" + clientId +
+                "* con PSP *" + pspId +
+                "* e pagato con " + formatPaymentTypeCode(paymentTypeCode);
 
         return createTextBlock("section", "mrkdwn", text, false);
     }
 
     static Map<String, Object> createStatusDetailsSection(AggregatedStatusGroup group) {
+        String details = formatStatusDetails(group.getStatusCounts());
+        if (details.trim().isEmpty()) {
+            details = "Nessun dettaglio disponibile.";
+        }
+
         return createTextBlock(
                 "section",
                 "mrkdwn",
-                formatStatusDetails(group.getStatusCounts()),
+                details,
                 false
         );
     }
@@ -205,6 +324,11 @@ static Map<String, Object> createTextBlock(
                                                String content,
                                                boolean emoji
     ) {
+        // Don't create blocks with empty content
+        if (content == null || content.trim().isEmpty()) {
+            return null;
+        }
+
         Map<String, Object> block = new HashMap<>();
         block.put("type", blockType);
 
diff --git a/src/test/java/it/pagopa/ecommerce/reporting/clients/SlackWebhookClientTest.java b/src/test/java/it/pagopa/ecommerce/reporting/clients/SlackWebhookClientTest.java
index 793a175..e34e9a4 100644
--- a/src/test/java/it/pagopa/ecommerce/reporting/clients/SlackWebhookClientTest.java
+++ b/src/test/java/it/pagopa/ecommerce/reporting/clients/SlackWebhookClientTest.java
@@ -164,7 +164,7 @@ void testPostRawJsonToWebhookWithErrorResponse() throws Exception {
         slackWebhookClient.postRawJsonToWebhook(validJson);
 
         verify(httpClientMock).execute(any(HttpPost.class));
-        verify(loggerMock).info("Error response from Slack: invalid_payload");
+        verify(loggerMock).info("Error response from Slack: invalid_payload, " + validJson);
     }
 
     @Test
diff --git a/src/test/java/it/pagopa/ecommerce/reporting/functions/SlackReportingTimerTriggeredTest.java b/src/test/java/it/pagopa/ecommerce/reporting/functions/SlackReportingTimerTriggeredTest.java
index 5b6f613..6c92816 100644
--- a/src/test/java/it/pagopa/ecommerce/reporting/functions/SlackReportingTimerTriggeredTest.java
+++ b/src/test/java/it/pagopa/ecommerce/reporting/functions/SlackReportingTimerTriggeredTest.java
@@ -1,5 +1,6 @@
 package it.pagopa.ecommerce.reporting.functions;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
 import com.microsoft.azure.functions.ExecutionContext;
 import it.pagopa.ecommerce.reporting.clients.SlackWebhookClient;
 import it.pagopa.ecommerce.reporting.services.TransactionStatusAggregationService;
@@ -13,13 +14,17 @@
 
 import java.time.DayOfWeek;
 import java.time.LocalDate;
+import java.time.LocalDateTime;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.logging.Logger;
 
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.*;
 import static org.mockito.ArgumentMatchers.*;
 import static org.mockito.Mockito.*;
 
@@ -47,11 +52,6 @@ class SlackReportingTimerTriggeredTest {
     @Captor
     private ArgumentCaptor<String> reportCaptor;
 
-    @BeforeEach
-    void setUp() {
-        when(mockContext.getLogger()).thenReturn(mockLogger);
-    }
-
     /**
      * Test class that extends the original class to allow for mocking
      */
@@ -98,79 +98,86 @@ protected SlackWebhookClient createSlackWebhookClient(String endpoint) {
     }
 
     @Test
-    void shouldUseCorrectDateRangeForLastWeek() throws Exception {
+    void shouldReturnCurrentDate() {
+        // Given
+        SlackReportingTimerTriggered function = new SlackReportingTimerTriggered();
+
+        // When
+        LocalDate result = function.getCurrentDate();
+
+        // Then
+        // LocalDate.now() will give the current date, which might change during
+        // test execution.
+        // So we verify it's not null and is "close to now"
+        assertNotNull(result, "Current date should not be null");
+        LocalDate today = LocalDate.now();
+        // Check if the date is within 1 day of today (to handle timezone differences)
+        assertTrue(
+                Math.abs(result.toEpochDay() - today.toEpochDay()) <= 1,
+                "Current date should be close to today"
+        );
+    }
 
-        String timerInfo = "timer info";
-        String mockEndpoint = "https://hooks.slack-mock.com/services/test/webhook";
+    @Test
+    void shouldParseValidDateFormat() {
+        // Given
+        SlackReportingTimerTriggered function = new SlackReportingTimerTriggered();
+        String dateString = "15-4-2023"; // day-month-year format
+        LocalDate defaultDate = LocalDate.of(2023, 1, 1);
+
+        // When
+        LocalDate result = function.getDateFromString(dateString, defaultDate);
+
+        // Then
+        LocalDate expected = LocalDate.now().withYear(2023).withMonth(4).withDayOfMonth(15);
+        assertEquals(expected, result, "Should correctly parse valid date format");
+    }
 
-        // Test with different days of the week to ensure correct "last week"
-        // calculation
-        LocalDate[] testDates = {
-                LocalDate.of(2025, 4, 21), // Monday
-                LocalDate.of(2025, 4, 22), // Tuesday
-                LocalDate.of(2025, 4, 23), // Wednesday
-                LocalDate.of(2025, 4, 24), // Thursday
-                LocalDate.of(2025, 4, 25), // Friday
-                LocalDate.of(2025, 4, 26), // Saturday
-                LocalDate.of(2025, 4, 27) // Sunday
-        };
+    @Test
+    void shouldReturnDefaultDateForInvalidFormat() {
+        // Given
+        SlackReportingTimerTriggered function = new SlackReportingTimerTriggered();
+        String dateString = "invalid-date-format";
+        LocalDate defaultDate = LocalDate.of(2023, 1, 1);
 
-        for (LocalDate today : testDates) {
-            // Calculate expected date range
-            LocalDate expectedLastMonday = today.minusWeeks(1).with(DayOfWeek.MONDAY);
-            LocalDate expectedLastSunday = expectedLastMonday.with(DayOfWeek.SUNDAY);
+        // When
+        LocalDate result = function.getDateFromString(dateString, defaultDate);
 
-            // Mock the aggregation service to return empty results
-            when(
-                    mockAggregationService.aggregateStatusCountByDateRange(
-                            startDateCaptor.capture(),
-                            endDateCaptor.capture(),
-                            eq(mockLogger)
-                    )
-            )
-                    .thenReturn(new ArrayList<>());
+        // Then
+        assertEquals(defaultDate, result, "Should return default date for invalid format");
+    }
 
-            // Mock the report creation
-            try (MockedStatic<SlackDateRangeReportMessageUtils> mockedReportUtils = Mockito
-                    .mockStatic(SlackDateRangeReportMessageUtils.class)) {
-                mockedReportUtils.when(
-                        () -> SlackDateRangeReportMessageUtils.createAggregatedWeeklyReport(
-                                any(),
-                                any(),
-                                any(),
-                                any()
-                        )
-                ).thenReturn("Report");
+    @Test
+    void shouldReturnDefaultDateForIncompleteComponents() {
+        // Given
+        SlackReportingTimerTriggered function = new SlackReportingTimerTriggered();
+        String dateString = "15-4"; // Missing year
+        LocalDate defaultDate = LocalDate.of(2023, 1, 1);
 
-                // Create testable instance
-                TestableSlackReportingTimerTriggered function = new TestableSlackReportingTimerTriggered(
-                        mockEndpoint,
-                        today,
-                        mockAggregationService,
-                        mockSlackWebhookClient
-                );
+        // When
+        LocalDate result = function.getDateFromString(dateString, defaultDate);
 
-                function.run(timerInfo, mockContext);
+        // Then
+        assertEquals(defaultDate, result, "Should return default date for incomplete components");
+    }
 
-                assertEquals(
-                        expectedLastMonday,
-                        startDateCaptor.getValue(),
-                        "Start date should be last Monday for current date: " + today
-                );
-                assertEquals(
-                        expectedLastSunday,
-                        endDateCaptor.getValue(),
-                        "End date should be last Sunday for current date: " + today
-                );
+    @Test
+    void shouldReturnDefaultDateForNumberFormatException() {
+        // Given
+        SlackReportingTimerTriggered function = new SlackReportingTimerTriggered();
+        String dateString = "15-Apr-2023"; // Non-numeric month
+        LocalDate defaultDate = LocalDate.of(2023, 1, 1);
 
-                // Reset mocks for next iteration
-                reset(mockAggregationService, mockSlackWebhookClient);
-            }
-        }
+        // When
+        LocalDate result = function.getDateFromString(dateString, defaultDate);
+
+        // Then
+        assertEquals(defaultDate, result, "Should return default date for number format exception");
     }
 
     @Test
     void shouldUseCorrectEndpointFromEnvironment() throws Exception {
+        when(mockContext.getLogger()).thenReturn(mockLogger);
 
         String timerInfo = "timer info";
         LocalDate fixedToday = LocalDate.of(2025, 4, 21);
@@ -190,7 +197,11 @@ void shouldUseCorrectEndpointFromEnvironment() throws Exception {
                             any(),
                             any()
                     )
-            ).thenReturn("Report");
+            ).thenReturn(
+                    new String[] {
+                            "Report"
+                    }
+            );
 
             // Create testable instance
             TestableSlackReportingTimerTriggered function = new TestableSlackReportingTimerTriggered(
@@ -200,16 +211,33 @@ void shouldUseCorrectEndpointFromEnvironment() throws Exception {
                     mockSlackWebhookClient
             );
 
-            function.run(timerInfo, mockContext);
+            // Check if the scheduler is properly closed
+            try (MockedStatic<Executors> executorsMock = Mockito.mockStatic(Executors.class)) {
+                // Mock the scheduler creation
+                ScheduledExecutorService mockScheduler = mock(ScheduledExecutorService.class);
+                executorsMock.when(Executors::newSingleThreadScheduledExecutor).thenReturn(mockScheduler);
+
+                // Mock the scheduler methods to execute tasks immediately
+                doAnswer(invocation -> {
+                    Runnable runnable = invocation.getArgument(0);
+                    runnable.run(); // Execute immediately
+                    return null;
+                }).when(mockScheduler).schedule(any(Runnable.class), anyLong(), any(TimeUnit.class));
+
+                function.run(timerInfo, mockContext);
+
+                // Verify that the SlackWebhookClient was called with the report
+                verify(mockSlackWebhookClient).postMessageToWebhook(anyString());
 
-            // Verify that the SlackWebhookClient was called with the report
-            verify(mockSlackWebhookClient).postMessageToWebhook(anyString());
+            }
         }
     }
 
     @Test
     void shouldLogAppropriateMessages() throws Exception {
+        when(mockContext.getLogger()).thenReturn(mockLogger);
 
+        // Given
         String timerInfo = "timer info";
         LocalDate fixedToday = LocalDate.of(2025, 4, 21); // A Monday
         LocalDate expectedLastMonday = fixedToday.minusWeeks(1);
@@ -260,7 +288,11 @@ void shouldLogAppropriateMessages() throws Exception {
                             any(Logger.class)
                     )
             )
-                    .thenReturn(mockReport);
+                    .thenReturn(
+                            new String[] {
+                                    mockReport
+                            }
+                    );
 
             // Create testable instance
             TestableSlackReportingTimerTriggered function = new TestableSlackReportingTimerTriggered(
@@ -270,47 +302,54 @@ void shouldLogAppropriateMessages() throws Exception {
                     mockSlackWebhookClient
             );
 
-            function.run(timerInfo, mockContext);
+            // Ensure that the scheduler is properly closed
+            try (MockedStatic<Executors> executorsMock = Mockito.mockStatic(Executors.class)) {
+                // Mock the scheduler creation
+                ScheduledExecutorService mockScheduler = mock(ScheduledExecutorService.class);
+                executorsMock.when(Executors::newSingleThreadScheduledExecutor).thenReturn(mockScheduler);
 
-            // Verify execution start log
-            verify(mockLogger).info(matches("Java Timer trigger SlackReportingTimerTriggered executed at: .*"));
+                // Mock the scheduler methods to execute tasks immediately
+                doAnswer(invocation -> {
+                    Runnable runnable = invocation.getArgument(0);
+                    runnable.run(); // Execute immediately
+                    return null;
+                }).when(mockScheduler).schedule(any(Runnable.class), anyLong(), any(TimeUnit.class));
 
-            // Verify date range and results count log
-            verify(mockLogger).info(
-                    "Start date: " + expectedLastMonday + " to date: " + expectedLastSunday +
-                            ", results: " + mockAggregatedStatuses.size()
-            );
+                function.run(timerInfo, mockContext);
+
+                // Verify execution start log
+                verify(mockLogger).info(matches("Java Timer trigger SlackReportingTimerTriggered executed at: .*"));
+
+                // Verify date range and results count log
+                verify(mockLogger).info(
+                        "Start date: " + expectedLastMonday + " to date: " + expectedLastSunday +
+                                ", results: " + mockAggregatedStatuses.size()
+                );
+
+                // Verify the webhook client was called with the correct report
+                verify(mockSlackWebhookClient).postMessageToWebhook(mockReport);
 
-            // Verify the webhook client was called with the correct report
-            verify(mockSlackWebhookClient).postMessageToWebhook(mockReport);
+                // Verify scheduler logs
+                verify(mockLogger).info("Sending 1 messages to Slack");
+                verify(mockLogger).info("Start read and write");
+                verify(mockLogger).info("Sending message 1 of 1");
+                verify(mockLogger).info("All messages sent successfully");
+            }
         }
     }
 
     @Test
     void shouldHandleMissingEndpointEnvironmentVariable() {
-
         String timerInfo = "timer info";
         LocalDate fixedToday = LocalDate.of(2025, 4, 21);
 
         // Create a testable instance that returns null for the endpoint
-        // AND returns null for the SlackWebhookClient to simulate the real behavior
         TestableSlackReportingTimerTriggered function = new TestableSlackReportingTimerTriggered(
                 null,
                 fixedToday,
                 mockAggregationService,
                 null
-        ) {
-            @Override
-            protected SlackWebhookClient createSlackWebhookClient(String endpoint) {
-                // This simulates the real behavior - when endpoint is null,
-                // passing it to the SlackWebhookClient constructor would throw
-                // NullPointerException
-                if (endpoint == null) {
-                    throw new NullPointerException("Endpoint cannot be null");
-                }
-                return mockSlackWebhookClient;
-            }
-        };
+        );
 
         // The function should throw NullPointerException when endpoint is null
         assertThrows(NullPointerException.class, () -> function.run(timerInfo, mockContext));
@@ -318,6 +357,7 @@ protected SlackWebhookClient createSlackWebhookClient(String endpoint) {
 
     @Test
     void shouldHandleExceptionFromAggregationService() throws Exception {
+        when(mockContext.getLogger()).thenReturn(mockLogger);
 
         String timerInfo = "timer info";
         LocalDate fixedToday = LocalDate.of(2025, 4, 21);
@@ -335,14 +375,449 @@ void shouldHandleExceptionFromAggregationService() throws Exception {
                 mockSlackWebhookClient
         );
 
-        assertThrows(RuntimeException.class, () -> function.run(timerInfo, mockContext));
+        // Ensure the scheduler is properly closed
+        try (MockedStatic<Executors> executorsMock = Mockito.mockStatic(Executors.class)) {
+            // Mock the scheduler creation
+            ScheduledExecutorService mockScheduler = mock(ScheduledExecutorService.class);
+            executorsMock.when(Executors::newSingleThreadScheduledExecutor).thenReturn(mockScheduler);
+
+            assertThrows(RuntimeException.class, () -> function.run(timerInfo, mockContext));
+
+            // Verify the webhook client was not called
+            verify(mockSlackWebhookClient, never()).postMessageToWebhook(anyString());
+
+            // Verify scheduler was never used for scheduling tasks
+            verify(mockScheduler, never()).schedule(any(Runnable.class), anyLong(), any(TimeUnit.class));
+        }
+    }
+
+    @Test
+    void shouldSendMultipleMessagesWhenReportIsSplit() throws Exception {
+        when(mockContext.getLogger()).thenReturn(mockLogger);
+
+        // Given
+        String timerInfo = "timer info";
+        LocalDate fixedToday = LocalDate.of(2025, 4, 21); // A Monday
+        LocalDate expectedLastMonday = fixedToday.minusWeeks(1);
+        LocalDate expectedLastSunday = expectedLastMonday.with(DayOfWeek.SUNDAY);
+        String mockEndpoint = "https://hooks.slack-mock.com/services/test/webhook";
+
+        // Large list that would result in multiple messages
+        List<AggregatedStatusGroup> mockAggregatedStatuses = new ArrayList<>();
+        List<String> statusFields = Arrays.asList("ACTIVATED", "CLOSED", "NOTIFIED_OK", "EXPIRED");
+
+        // Add enough groups to exceed the 50 block limit
+        for (int i = 0; i < 20; i++) {
+            AggregatedStatusGroup group = new AggregatedStatusGroup(
+                    "2023-05-" + (i < 10 ? "0" + i : i),
+                    "client" + i,
+                    "psp" + i,
+                    "PT" + i,
+                    statusFields
+            );
+            group.incrementStatus("ACTIVATED", 100 - i);
+            group.incrementStatus("CLOSED", 50 - i);
+            group.incrementStatus("NOTIFIED_OK", 30 - i);
+            group.incrementStatus("EXPIRED", i);
+            mockAggregatedStatuses.add(group);
+        }
+
+        // Mock the aggregation service to return our mock statuses
+        when(
+                mockAggregationService.aggregateStatusCountByDateRange(
+                        eq(expectedLastMonday),
+                        eq(expectedLastSunday),
+                        any(Logger.class)
+                )
+        )
+                .thenReturn(mockAggregatedStatuses);
+
+        // Create mock report messages - simulate multiple messages returned
+        String[] mockReportMessages = {
+                "First part of the report",
+                "Second part of the report",
+                "Third part of the report"
+        };
+
+        try (MockedStatic<SlackDateRangeReportMessageUtils> mockedReportUtils = Mockito
+                .mockStatic(SlackDateRangeReportMessageUtils.class)) {
+            mockedReportUtils.when(
+                    () -> SlackDateRangeReportMessageUtils.createAggregatedWeeklyReport(
+                            eq(mockAggregatedStatuses),
+                            eq(expectedLastMonday),
+                            eq(expectedLastSunday),
+                            any(Logger.class)
+                    )
+            )
+                    .thenReturn(mockReportMessages);
+
+            // Create testable instance
+            TestableSlackReportingTimerTriggered function = new TestableSlackReportingTimerTriggered(
+                    mockEndpoint,
+                    fixedToday,
+                    mockAggregationService,
+                    mockSlackWebhookClient
+            );
+
+            // Ensure the scheduler is properly closed
+            try (MockedStatic<Executors> executorsMock = Mockito.mockStatic(Executors.class)) {
+                // Mock the scheduler creation
+                ScheduledExecutorService mockScheduler = mock(ScheduledExecutorService.class);
+                executorsMock.when(Executors::newSingleThreadScheduledExecutor).thenReturn(mockScheduler);
+
+                // Mock the scheduler methods to execute tasks immediately
+                doAnswer(invocation -> {
+                    Runnable runnable = invocation.getArgument(0);
+                    runnable.run(); // Execute immediately
+                    return null;
+                }).when(mockScheduler).schedule(any(Runnable.class), anyLong(), any(TimeUnit.class));
 
-        // Verify the webhook client was not called
-        verify(mockSlackWebhookClient, never()).postMessageToWebhook(anyString());
+                // When
+                function.run(timerInfo, mockContext);
+
+                // Then
+                // Verify that the webhook client was called for each message
+                verify(mockSlackWebhookClient).postMessageToWebhook(mockReportMessages[0]);
+                verify(mockSlackWebhookClient).postMessageToWebhook(mockReportMessages[1]);
+                verify(mockSlackWebhookClient).postMessageToWebhook(mockReportMessages[2]);
+                verify(mockSlackWebhookClient, times(3)).postMessageToWebhook(anyString());
+
+                // Verify logging
+                verify(mockLogger).info("Sending 3 messages to Slack");
+                verify(mockLogger).info("Start read and write");
+                verify(mockLogger).info("Sending message 1 of 3");
+                verify(mockLogger).info("Sending message 2 of 3");
+                verify(mockLogger).info("Sending message 3 of 3");
+                verify(mockLogger).info("All messages sent successfully");
+            }
+        }
+    }
+
+    @Test
+    void shouldHandleExceptionDuringMultipleMessageSending() throws Exception {
+        when(mockContext.getLogger()).thenReturn(mockLogger);
+
+        // Given
+        String timerInfo = "timer info";
+        LocalDate fixedToday = LocalDate.of(2025, 4, 21);
+        String mockEndpoint = "https://hooks.slack-mock.com/services/test/webhook";
+
+        // Mock the aggregation service to return some results
+        when(mockAggregationService.aggregateStatusCountByDateRange(any(), any(), any()))
+                .thenReturn(new ArrayList<>());
+
+        // Create mock report messages
+        String[] mockReportMessages = {
+                "First part of the report",
+                "Second part of the report",
+                "Third part of the report"
+        };
+
+        try (MockedStatic<SlackDateRangeReportMessageUtils> mockedReportUtils = Mockito
+                .mockStatic(SlackDateRangeReportMessageUtils.class)) {
+            mockedReportUtils.when(
+                    () -> SlackDateRangeReportMessageUtils.createAggregatedWeeklyReport(
+                            any(),
+                            any(),
+                            any(),
+                            any()
+                    )
+            ).thenReturn(mockReportMessages);
+
+            // Create testable instance
+            TestableSlackReportingTimerTriggered function = new TestableSlackReportingTimerTriggered(
+                    mockEndpoint,
+                    fixedToday,
+                    mockAggregationService,
+                    mockSlackWebhookClient
+            );
+
+            // Ensure the scheduler is properly closed
+            try (MockedStatic<Executors> executorsMock = Mockito.mockStatic(Executors.class)) {
+                // Mock the scheduler creation
+                ScheduledExecutorService mockScheduler = mock(ScheduledExecutorService.class);
+                executorsMock.when(Executors::newSingleThreadScheduledExecutor).thenReturn(mockScheduler);
+
+                // Mock the scheduler methods
+                // For the first message, execute normally
+                // For the second message, throw an exception
+                // For the third message, do nothing (it should still be scheduled)
+                final AtomicInteger taskCounter = new AtomicInteger(0);
+                doAnswer(invocation -> {
+                    int currentTask = taskCounter.getAndIncrement();
+                    Runnable task = invocation.getArgument(0);
+
+                    if (currentTask == 0) {
+                        // First task should succeed
+                        task.run();
+                    } else if (currentTask == 1) {
+                        // Second task will throw exception
+                        doThrow(new RuntimeException("Error sending to Slack"))
+                                .when(mockSlackWebhookClient).postMessageToWebhook(mockReportMessages[1]);
+                        try {
+                            task.run();
+                        } catch (RuntimeException e) {
+                            // Expected exception, but we want to continue the test
+                            // so we catch it here and don't rethrow
+                        }
+                    }
+                    // Don't execute third task
+
+                    return null;
+                }).when(mockScheduler).schedule(any(Runnable.class), anyLong(), eq(TimeUnit.SECONDS));
+
+                // When
+                function.run(timerInfo, mockContext);
+
+                // Then
+                // Verify that the first message was sent successfully
+                verify(mockSlackWebhookClient).postMessageToWebhook(mockReportMessages[0]);
+
+                // Verify that the second message was attempted
+                verify(mockSlackWebhookClient).postMessageToWebhook(mockReportMessages[1]);
+
+                // Verify that tasks were scheduled
+                verify(mockScheduler, times(3)).schedule(any(Runnable.class), anyLong(), eq(TimeUnit.SECONDS));
+
+                // Verify logging - updated to match the new implementation
+                verify(mockLogger).info("Sending 3 messages to Slack");
+                verify(mockLogger).info("Start read and write");
+                verify(mockLogger).info("Sending message 2 of 3"); // The second message
+                verify(mockLogger).info("All messages sent successfully");
+            }
+        }
+    }
+
+    @Test
+    void shouldHandleInterruptedExceptionDuringAwaitTermination() throws Exception {
+        when(mockContext.getLogger()).thenReturn(mockLogger);
+
+        // Given
+        String timerInfo = "timer info";
+        LocalDate fixedToday = LocalDate.of(2025, 4, 21);
+        String mockEndpoint = "https://hooks.slack-mock.com/services/test/webhook";
+
+        // Mock the aggregation service to return some results
+        when(mockAggregationService.aggregateStatusCountByDateRange(any(), any(), any()))
+                .thenReturn(new ArrayList<>());
+
+        // Create mock report messages
+        String[] mockReportMessages = {
+                "First part of the report",
+                "Second part of the report"
+        };
+
+        try (MockedStatic<SlackDateRangeReportMessageUtils> mockedReportUtils = Mockito
+                .mockStatic(SlackDateRangeReportMessageUtils.class)) {
+            mockedReportUtils.when(
+                    () -> SlackDateRangeReportMessageUtils.createAggregatedWeeklyReport(
+                            any(),
+                            any(),
+                            any(),
+                            any()
+                    )
+            ).thenReturn(mockReportMessages);
+
+            // Create testable instance
+            TestableSlackReportingTimerTriggered function = new TestableSlackReportingTimerTriggered(
+                    mockEndpoint,
+                    fixedToday,
+                    mockAggregationService,
+                    mockSlackWebhookClient
+            );
+
+            // Ensure the scheduler is properly closed
+            try (MockedStatic<Executors> executorsMock = Mockito.mockStatic(Executors.class)) {
+                // Mock the scheduler creation
+                ScheduledExecutorService mockScheduler = mock(ScheduledExecutorService.class);
+                executorsMock.when(Executors::newSingleThreadScheduledExecutor).thenReturn(mockScheduler);
+
+                // Mock the scheduler methods to execute tasks immediately
+                doAnswer(invocation -> {
+                    Runnable task = invocation.getArgument(0);
+                    task.run(); // Execute the task immediately
+                    return null;
+                }).when(mockScheduler).schedule(any(Runnable.class), anyLong(), eq(TimeUnit.SECONDS));
+
+                // When
+                function.run(timerInfo, mockContext);
+
+                // Then
+                // Verify messages were sent
+                verify(mockSlackWebhookClient, times(2)).postMessageToWebhook(anyString());
+
+                // Verify logging - updated to match the new implementation
+                verify(mockLogger).info("Sending 2 messages to Slack");
+                verify(mockLogger).info("Start read and write");
+                verify(mockLogger).info("Sending message 1 of 2");
+                verify(mockLogger).info("Sending message 2 of 2");
+                verify(mockLogger).info("All messages sent successfully");
+
+                // No longer verifying warning about interruption since there's no
+                // awaitTermination
+            }
+        }
+    }
+
+    @Test
+    void shouldScheduleTasksWithCorrectDelays() throws Exception {
+        when(mockContext.getLogger()).thenReturn(mockLogger);
+
+        // Given
+        String timerInfo = "timer info";
+        LocalDate fixedToday = LocalDate.of(2025, 4, 21);
+        String mockEndpoint = "https://hooks.slack-mock.com/services/test/webhook";
+
+        // Mock the aggregation service to return some results
+        when(mockAggregationService.aggregateStatusCountByDateRange(any(), any(), any()))
+                .thenReturn(new ArrayList<>());
+
+        // Create mock report messages
+        String[] mockReportMessages = {
+                "First part of the report",
+                "Second part of the report",
+                "Third part of the report"
+        };
+
+        try (MockedStatic<SlackDateRangeReportMessageUtils> mockedReportUtils = Mockito
+                .mockStatic(SlackDateRangeReportMessageUtils.class)) {
+            mockedReportUtils.when(
+                    () -> SlackDateRangeReportMessageUtils.createAggregatedWeeklyReport(
+                            any(),
+                            any(),
+                            any(),
+                            any()
+                    )
+            ).thenReturn(mockReportMessages);
+
+            // Create testable instance
+            TestableSlackReportingTimerTriggered function = new TestableSlackReportingTimerTriggered(
+                    mockEndpoint,
+                    fixedToday,
+                    mockAggregationService,
+                    mockSlackWebhookClient
+            );
+
+            // Ensure the scheduler is properly closed
+            try (MockedStatic<Executors> executorsMock = Mockito.mockStatic(Executors.class)) {
+                // Mock the scheduler creation
+                ScheduledExecutorService mockScheduler = mock(ScheduledExecutorService.class);
+                executorsMock.when(Executors::newSingleThreadScheduledExecutor).thenReturn(mockScheduler);
+
+                // Capture the delay arguments
+                ArgumentCaptor<Long> delayCaptor = ArgumentCaptor.forClass(Long.class);
+
+                // When
+                function.run(timerInfo, mockContext);
+
+                // Then
+                // Verify that tasks were scheduled with increasing delays
+                verify(mockScheduler, times(3))
+                        .schedule(any(Runnable.class), delayCaptor.capture(), eq(TimeUnit.SECONDS));
+
+                List<Long> capturedDelays = delayCaptor.getAllValues();
+                assertEquals(3, capturedDelays.size());
+
+                assertEquals(1L, capturedDelays.get(0).longValue());
+                assertEquals(2L, capturedDelays.get(1).longValue());
+                assertEquals(3L, capturedDelays.get(2).longValue());
+            }
+        }
+    }
+
+    @Test
+    void shouldUseCorrectDateRangeForLastWeek() throws Exception {
+        when(mockContext.getLogger()).thenReturn(mockLogger);
+
+        String timerInfo = "timer info";
+        String mockEndpoint = "https://hooks.slack-mock.com/services/test/webhook";
+
+        // Test with different days of the week to ensure correct "last week"
+        // calculation
+        LocalDate[] testDates = {
+                LocalDate.of(2025, 4, 21), // Monday
+                LocalDate.of(2025, 4, 22), // Tuesday
+                LocalDate.of(2025, 4, 23), // Wednesday
+                LocalDate.of(2025, 4, 24), // Thursday
+                LocalDate.of(2025, 4, 25), // Friday
+                LocalDate.of(2025, 4, 26), // Saturday
+                LocalDate.of(2025, 4, 27) // Sunday
+        };
+
+        for (LocalDate today : testDates) {
+            // Calculate expected date range
+            LocalDate expectedLastMonday = today.minusWeeks(1).with(DayOfWeek.MONDAY);
+            LocalDate expectedLastSunday = expectedLastMonday.with(DayOfWeek.SUNDAY);
+
+            // Mock the aggregation service to return empty results
+            when(
+                    mockAggregationService.aggregateStatusCountByDateRange(
+                            startDateCaptor.capture(),
+                            endDateCaptor.capture(),
+                            eq(mockLogger)
+                    )
+            )
+                    .thenReturn(new ArrayList<>());
+
+            // Mock the report creation
+            try (MockedStatic<SlackDateRangeReportMessageUtils> mockedReportUtils = Mockito
+                    .mockStatic(SlackDateRangeReportMessageUtils.class)) {
+                mockedReportUtils.when(
+                        () -> SlackDateRangeReportMessageUtils.createAggregatedWeeklyReport(
+                                any(),
+                                any(),
+                                any(),
+                                any()
+                        )
+                ).thenReturn(
+                        new String[] {
+                                "Report"
+                        }
+                );
+
+                // Create testable instance
+                TestableSlackReportingTimerTriggered function = new TestableSlackReportingTimerTriggered(
+                        mockEndpoint,
+                        today,
+                        mockAggregationService,
+                        mockSlackWebhookClient
+                );
+
+                // Ensure the scheduler is properly closed
+                try (MockedStatic<Executors> executorsMock = Mockito.mockStatic(Executors.class)) {
+                    // Mock the scheduler creation
+                    ScheduledExecutorService mockScheduler = mock(ScheduledExecutorService.class);
+                    executorsMock.when(Executors::newSingleThreadScheduledExecutor).thenReturn(mockScheduler);
+
+                    // Don't execute tasks to avoid duplicate calls
+                    doReturn(null).when(mockScheduler).schedule(any(Runnable.class), anyLong(), any(TimeUnit.class));
+
+                    function.run(timerInfo, mockContext);
+
+                    assertEquals(
+                            expectedLastMonday,
+                            startDateCaptor.getValue(),
+                            "Start date should be last Monday for current date: " + today
+                    );
+                    assertEquals(
+                            expectedLastSunday,
+                            endDateCaptor.getValue(),
+                            "End date should be last Sunday for current date: " + today
+                    );
+
+                    // No need to verify scheduler.shutdown() since it's not called in the
+                    // implementation
+                }
+
+                // Reset mocks for next iteration
+                reset(mockAggregationService, mockSlackWebhookClient);
+            }
+        }
     }
 
     @Test
     void shouldHandleExceptionFromSlackClient() throws Exception {
+        when(mockContext.getLogger()).thenReturn(mockLogger);
 
         String timerInfo = "timer info";
         LocalDate fixedToday = LocalDate.of(2025, 4, 21);
@@ -362,10 +837,13 @@ void shouldHandleExceptionFromSlackClient() throws Exception {
                             any(),
                             any()
                     )
-            ).thenReturn("Report");
+            ).thenReturn(
+                    new String[] {
+                            "Report"
+                    }
+            );
 
-            // Mock the webhook client to throw a RuntimeException instead of
-            // JsonProcessingException
+            // Mock the webhook client to throw a RuntimeException
             doThrow(new RuntimeException("Error sending to Slack")).when(mockSlackWebhookClient)
                     .postMessageToWebhook(anyString());
 
@@ -377,8 +855,26 @@ void shouldHandleExceptionFromSlackClient() throws Exception {
                     mockSlackWebhookClient
             );
 
-            // Since we're throwing RuntimeException, we should expect an exception
-            assertThrows(RuntimeException.class, () -> function.run(timerInfo, mockContext));
+            // Ensure the scheduler is properly closed
+            try (MockedStatic<Executors> executorsMock = Mockito.mockStatic(Executors.class)) {
+                // Mock the scheduler creation
+                ScheduledExecutorService mockScheduler = mock(ScheduledExecutorService.class);
+                executorsMock.when(Executors::newSingleThreadScheduledExecutor).thenReturn(mockScheduler);
+
+                // Mock the scheduler methods to execute tasks immediately
+                doAnswer(invocation -> {
+                    Runnable runnable = invocation.getArgument(0);
+                    runnable.run(); // This will throw the exception from the webhook client
+                    return null;
+                }).when(mockScheduler).schedule(any(Runnable.class), anyLong(), any(TimeUnit.class));
+
+                // Since we're throwing RuntimeException, we should expect an exception
+                assertThrows(RuntimeException.class, () -> function.run(timerInfo, mockContext));
+
+                // Verify scheduler interaction but don't verify shutdown() since it's not
+                // called in the implementation
+                verify(mockScheduler).schedule(any(Runnable.class), anyLong(), any(TimeUnit.class));
+            }
         }
     }
 }
diff --git a/src/test/java/it/pagopa/ecommerce/reporting/services/TransactionStatusAggregationServiceTest.java b/src/test/java/it/pagopa/ecommerce/reporting/services/TransactionStatusAggregationServiceTest.java
index 215728c..ed293a3 100644
--- a/src/test/java/it/pagopa/ecommerce/reporting/services/TransactionStatusAggregationServiceTest.java
+++ b/src/test/java/it/pagopa/ecommerce/reporting/services/TransactionStatusAggregationServiceTest.java
@@ -76,6 +76,61 @@ void testAggregateStatusCountByDateRange() {
         assertEquals(6, counts.get("ACTIVATED"));
         assertEquals(6, counts.get("CLOSED"));
         assertEquals(11, counts.get("NOTIFIED_OK"));
-        assertEquals(0, counts.get("EXPIRED"));
+
+        // EXPIRED shouldn't be in the map (0 occurrences)
+        assertFalse(counts.containsKey("EXPIRED"));
+    }
+
+    @Test
+    void testAggregateStatusCountByDateRangeFiltersZeroCounts() {
+        // Given
+        LocalDate startDate = LocalDate.of(2024, 4, 1);
+        LocalDate endDate = LocalDate.of(2024, 4, 1);
+        String partitionKey = "2024-04-01";
+
+        TableEntity entity = new TableEntity(partitionKey, "row1");
+        entity.addProperty("clientId", "clientA");
+        entity.addProperty("pspId", "pspX");
+        entity.addProperty("paymentTypeCode", "PT1");
+        entity.addProperty("ACTIVATED", 5);
+        entity.addProperty("CLOSED", 0);
+        entity.addProperty("NOTIFIED_OK", 3);
+        entity.addProperty("EXPIRED", 0);
+
+        Iterable<TableEntity> iterable = List.of(entity);
+
+        // Mocking the PagedIterable with an iterator
+        PagedIterable<TableEntity> pagedEntities = mock(PagedIterable.class);
+        when(pagedEntities.iterator()).thenReturn(iterable.iterator());
+
+        when(mockTableClient.listEntities(any(ListEntitiesOptions.class), isNull(), isNull()))
+                .thenReturn(pagedEntities);
+
+        // When
+        List<AggregatedStatusGroup> result = service.aggregateStatusCountByDateRange(startDate, endDate, mockLogger);
+
+        // Then
+        assertEquals(1, result.size());
+
+        AggregatedStatusGroup group = result.get(0);
+        assertEquals("2024-04-01", group.getDate());
+        assertEquals("clientA", group.getClientId());
+        assertEquals("pspX", group.getPspId());
+        assertEquals("PT1", group.getPaymentTypeCode());
+
+        Map<String, Integer> counts = group.getStatusCounts();
+
+        // Verify that statuses with non-zero counts are present
+        assertTrue(counts.containsKey("ACTIVATED"));
+        assertEquals(5, counts.get("ACTIVATED"));
+        assertTrue(counts.containsKey("NOTIFIED_OK"));
+        assertEquals(3, counts.get("NOTIFIED_OK"));
+
+        // Verify that statuses with zero counts are filtered out
+        assertFalse(counts.containsKey("CLOSED"));
+        assertFalse(counts.containsKey("EXPIRED"));
+
+        // Verify the size of the map (should only contain the non-zero statuses)
+        assertEquals(2, counts.size());
     }
 }
diff --git a/src/test/java/it/pagopa/ecommerce/reporting/utils/SlackDateRangeReportMessageUtilsTest.java b/src/test/java/it/pagopa/ecommerce/reporting/utils/SlackDateRangeReportMessageUtilsTest.java
index 0c0d1b9..14a161e 100644
--- a/src/test/java/it/pagopa/ecommerce/reporting/utils/SlackDateRangeReportMessageUtilsTest.java
+++ b/src/test/java/it/pagopa/ecommerce/reporting/utils/SlackDateRangeReportMessageUtilsTest.java
@@ -1,5 +1,7 @@
 package it.pagopa.ecommerce.reporting.utils;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.Mock;
@@ -201,8 +203,6 @@ void shouldCreateCorrectImageBlock() {
         assertEquals("image", result.get("type"));
         assertTrue(result.get("image_url").toString().contains("logo_asset.png"));
         assertEquals("PagoPA Logo", result.get("alt_text"));
-        assertEquals(474, result.get("image_width"));
-        assertEquals(133, result.get("image_height"));
     }
 
     @Test
@@ -326,58 +326,83 @@ void shouldCreateCompleteReport() throws Exception {
         LocalDate endDate = LocalDate.of(2023, 1, 7);
 
         // When
-        String result = SlackDateRangeReportMessageUtils
+        String[] results = SlackDateRangeReportMessageUtils
                 .createAggregatedWeeklyReport(groups, startDate, endDate, mockLogger);
 
         // Then
-        assertNotNull(result);
+        assertNotNull(results);
+        assertTrue(results.length > 0, "Should return at least one message");
 
-        // Print the result for debugging
-        System.out.println("Report result: " + result);
+        // Print the results for debugging
+        for (int i = 0; i < results.length; i++) {
+            System.out.println("Report result " + (i + 1) + ": " + results[i]);
+        }
 
-        // Check basic structure
-        assertTrue(result.contains("blocks"), "Result should contain 'blocks'");
+        // Check first message structure
+        String firstMessage = results[0];
+        assertTrue(firstMessage.contains("blocks"), "Result should contain 'blocks'");
 
         // Check date formatting
         String formattedStartDate = SlackDateRangeReportMessageUtils.formatDate(startDate);
         String formattedEndDate = SlackDateRangeReportMessageUtils.formatDate(endDate);
+
         assertTrue(
-                result.contains(formattedStartDate),
+                firstMessage.contains(formattedStartDate),
                 "Result should contain formatted start date: " + formattedStartDate
         );
         assertTrue(
-                result.contains(formattedEndDate),
+                firstMessage.contains(formattedEndDate),
                 "Result should contain formatted end date: " + formattedEndDate
         );
 
-        // Check client and PSP IDs - these might be formatted differently in the actual
-        // output
-        assertTrue(
-                result.contains("client1") || result.contains("*client1*"),
-                "Result should contain client1"
-        );
-        assertTrue(
-                result.contains("client2") || result.contains("*client2*"),
-                "Result should contain client2"
-        );
-        assertTrue(
-                result.contains("psp1") || result.contains("*psp1*"),
-                "Result should contain psp1"
-        );
-        assertTrue(
-                result.contains("psp2") || result.contains("*psp2*"),
-                "Result should contain psp2"
-        );
+        // Check for client and PSP IDs across all messages
+        boolean foundClient1 = false;
+        boolean foundClient2 = false;
+        boolean foundPsp1 = false;
+        boolean foundPsp2 = false;
+        boolean foundCP = false;
+        boolean foundPPAL = false;
+
+        for (String result : results) {
+            if (result.contains("client1") || result.contains("\\*client1\\*"))
+                foundClient1 = true;
+            if (result.contains("client2") || result.contains("\\*client2\\*"))
+                foundClient2 = true;
+            if (result.contains("psp1") || result.contains("\\*psp1\\*"))
+                foundPsp1 = true;
+            if (result.contains("psp2") || result.contains("\\*psp2\\*"))
+                foundPsp2 = true;
+            if (result.contains("CP") || result.contains("Carte"))
+                foundCP = true;
+            if (result.contains("PPAL") || result.contains("PayPal"))
+                foundPPAL = true;
+        }
+
+        assertTrue(foundClient1, "Results should contain client1");
+        assertTrue(foundClient2, "Results should contain client2");
+        assertTrue(foundPsp1, "Results should contain psp1");
+        assertTrue(foundPsp2, "Results should contain psp2");
+        assertTrue(foundCP, "Results should contain CP or its translation");
+        assertTrue(foundPPAL, "Results should contain PPAL or its translation");
+
+        // Verify each message has at most MAX_BLOCKS_PER_MESSAGE blocks
+        for (String result : results) {
+            int blockCount = countOccurrences(result);
+            assertTrue(
+                    blockCount <= 50,
+                    "Each message should have at most 50 blocks, but found " + blockCount
+            );
+        }
+    }
 
-        // Check payment types - these might be translated in the output
-        assertTrue(
-                result.contains("CP") || result.contains("Carte"),
-                "Result should contain CP or its translation"
-        );
-        assertTrue(
-                result.contains("PPAL") || result.contains("PayPal"),
-                "Result should contain PPAL or its translation"
-        );
+    // Helper method to count number of blocks
+    private int countOccurrences(
+                                 String jsonStr
+    ) throws JsonProcessingException {
+        ObjectMapper objectMapper = new ObjectMapper();
+        Map<String, Object> messageMap = objectMapper.readValue(jsonStr, Map.class);
+        List<Object> blocks = (List<Object>) messageMap.get("blocks");
+        return blocks.size();
     }
 
     @Test
@@ -388,11 +413,14 @@ void shouldHandleEmptyGroups() throws Exception {
         LocalDate endDate = LocalDate.of(2023, 1, 7);
 
         // When
-        String result = SlackDateRangeReportMessageUtils
+        String[] results = SlackDateRangeReportMessageUtils
                 .createAggregatedWeeklyReport(groups, startDate, endDate, mockLogger);
 
         // Then
-        assertNotNull(result);
+        assertNotNull(results);
+        assertEquals(1, results.length, "Should return exactly one message for empty groups");
+
+        String result = results[0];
         assertTrue(result.contains("blocks"));
         assertTrue(result.contains("1 gennaio 2023"));
         assertTrue(result.contains("7 gennaio 2023"));
@@ -482,4 +510,97 @@ void shouldInitializeStatusCountsWithZeros() {
         assertEquals(0, group.getStatusCounts().get("EXPIRED"));
         assertEquals(3, group.getStatusCounts().size());
     }
+
+    @Test
+    void shouldSplitReportIntoMultipleMessagesWhenExceedingMaxBlocks() throws Exception {
+        // Given
+        List<AggregatedStatusGroup> groups = new ArrayList<>();
+
+        // Create enough groups to exceed the 50 block limit
+        for (int i = 0; i < 20; i++) {
+            AggregatedStatusGroup group = new AggregatedStatusGroup(
+                    "2023-01-01",
+                    "client" + i,
+                    "psp" + i,
+                    "CP",
+                    Arrays.asList("ACTIVATED", "NOTIFIED_OK", "EXPIRED")
+            );
+            group.incrementStatus("ACTIVATED", 100 - i);
+            group.incrementStatus("NOTIFIED_OK", 80 - i);
+            group.incrementStatus("EXPIRED", i);
+
+            groups.add(group);
+        }
+
+        LocalDate startDate = LocalDate.of(2023, 1, 1);
+        LocalDate endDate = LocalDate.of(2023, 1, 7);
+
+        // When
+        String[] results = SlackDateRangeReportMessageUtils
+                .createAggregatedWeeklyReport(groups, startDate, endDate, mockLogger);
+
+        // Then
+        assertNotNull(results);
+        assertTrue(results.length > 1, "Should split into multiple messages");
+
+        // Check that each message has at most 50 blocks
+        for (String result : results) {
+            int blockCount = countOccurrences(result);
+            assertTrue(
+                    blockCount <= 50,
+                    "Each message should have at most 50 blocks, but found " + blockCount
+            );
+        }
+
+        // First message should contain header information
+        assertTrue(
+                results[0].contains("Report Settimanale Transazioni"),
+                "First message should contain report header"
+        );
+    }
+
+    @Test
+    void shouldIncludeAllGroupsAcrossMessages() throws Exception {
+        // Given
+        List<AggregatedStatusGroup> groups = new ArrayList<>();
+
+        // Create groups with distinct identifiable names
+        for (int i = 0; i < 15; i++) {
+            String uniqueId = "UNIQUE_ID_" + i;
+            AggregatedStatusGroup group = new AggregatedStatusGroup(
+                    "2023-01-01",
+                    uniqueId,
+                    "psp" + i,
+                    "CP",
+                    Arrays.asList("ACTIVATED", "NOTIFIED_OK")
+            );
+            group.incrementStatus("ACTIVATED", 100 - i);
+            group.incrementStatus("NOTIFIED_OK", 80 - i);
+
+            groups.add(group);
+        }
+
+        LocalDate startDate = LocalDate.of(2023, 1, 1);
+        LocalDate endDate = LocalDate.of(2023, 1, 7);
+
+        // When
+        String[] results = SlackDateRangeReportMessageUtils
+                .createAggregatedWeeklyReport(groups, startDate, endDate, mockLogger);
+
+        // Then
+        // Verify all unique IDs are present across all messages
+        for (int i = 0; i < 15; i++) {
+            String uniqueId = "UNIQUE_ID_" + i;
+            boolean foundId = false;
+
+            for (String result : results) {
+                if (result.contains(uniqueId)) {
+                    foundId = true;
+                    break;
+                }
+            }
+
+            assertTrue(foundId, "Group with ID " + uniqueId + " should be included in at least one message");
+        }
+    }
 }


Reviews:
user: login: ciuffagianluca
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-26T21:45:52Z
user: login: ciuffagianluca
body: Please fix junit tests
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-26T21:48:30Z
user: login: ciuffagianluca
body: Just an idea to open some discussions about the message format.
The message now has the format of a list of all transaction status for each client/paymentMethod/psp. Since this format turned out to be too much long for a single slack message, in this PR we decided to split this message in more messages to show all info we retrieved. IMHO I think that the readability of multiple messages like these could be really conditioned by this format. So I'm thinking about two possible different format:
1. a message format like the one implemented now, but without entry with 0 occurences for transaction status (not my preferred, but for sure very simple to implement)
2. a message format more compact that shows for each client/paymentMethod (no pspId you read rightly) status counter aggregated by notified_ok/notification_requested (ok status), notified_ko and other final status (ko status), and an aggregator counter for all non final statuses. We can consider to implement an httpTriggered Function to have a detailed status report for a predefined period to retrieve (if needed) more info about a specific date interval (and also client/paymentMethod/psp filters available).

What do you think?


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-26T21:58:22Z
user: login: sfarralorenzo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-27T10:17:45Z
user: login: ciuffagianluca
body: 🚀 well done 🎸 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-27T16:39:47Z

Review comments:
diff_hunk: @@ -35,25 +33,69 @@ public void run(
         logger.info("Java Timer trigger SlackReportingTimerTriggered executed at: " + LocalDateTime.now());
 
         String endpoint = getEnvVariable("ECOMMERCE_SLACK_REPORTING_WEBHOOK_ENDPOINT");
+        String reportStartDate = getEnvVariable("REPORT_START_DATE");
+        String reportEndDate = getEnvVariable("REPORT_END_DATE");
 
         SlackWebhookClient slackWebhookClient = createSlackWebhookClient(endpoint);
         LocalDate today = getCurrentDate();
-        LocalDate lastMonday = today.minusWeeks(1).with(DayOfWeek.MONDAY);
-        LocalDate lastSunday = lastMonday.with(DayOfWeek.SUNDAY);
+
+        LocalDate startDate = today.minusWeeks(1).with(DayOfWeek.MONDAY);
+        LocalDate endDate = startDate.with(DayOfWeek.SUNDAY);
+
+        if (reportStartDate != null && reportEndDate != null) {
+            startDate = getDateFromString(reportStartDate, startDate);
+            endDate = getDateFromString(reportEndDate, endDate);
+        }
 
         TransactionStatusAggregationService transactionStatusAggregationService = createAggregationService();
         List<AggregatedStatusGroup> aggregatedStatuses = transactionStatusAggregationService
-                .aggregateStatusCountByDateRange(lastMonday, lastSunday, logger);
+                .aggregateStatusCountByDateRange(startDate, endDate, logger);
 
         logger.info(
-                "Start date: " + lastMonday + " to date: " + lastSunday + ", results: " + aggregatedStatuses.size()
+                "Start date: " + startDate + " to date: " + endDate + ", results: " + aggregatedStatuses.size()
         );
 
-        // Create the report message
-        String report = SlackDateRangeReportMessageUtils
-                .createAggregatedWeeklyReport(aggregatedStatuses, lastMonday, lastSunday, logger);
-        // Send it to Slack
-        slackWebhookClient.postMessageToWebhook(report);
+        // Create the report messages
+        String[] reportMessages = SlackDateRangeReportMessageUtils
+                .createAggregatedWeeklyReport(aggregatedStatuses, startDate, endDate, logger);
+
+        // Send each message to Slack
+        logger.info("Sending " + reportMessages.length + " messages to Slack");
+        for (int i = 0; i < reportMessages.length; i++) {
+            logger.info("Sending message " + (i + 1) + " of " + reportMessages.length);
+            slackWebhookClient.postMessageToWebhook(reportMessages[i]);
+
+            // Add a small delay between messages to avoid rate limiting
+            if (i < reportMessages.length - 1) {
+                try {
+                    Thread.sleep(1000); // 1-second delay
+                } catch (InterruptedException e) {
+                    logger.warning("Sleep interrupted: " + e.getMessage());
+                    Thread.currentThread().interrupt();
+                }
+            }
+        }
+
+        logger.info("All messages sent successfully");
path: src/main/java/it/pagopa/ecommerce/reporting/functions/SlackReportingTimerTriggered.java
user: login: ciuffagianluca
body: Please consider to use Executor service to schedule delayed tasks to run in place of `Thread.sleep()` interrupts. 

```suggestion
        ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
        AtomicInteger index = new AtomicInteger(0);
        Arrays.stream(reportMessages).map(message -> new TimerTask() {
            @Override
            public void run() {
                logger.info("Sending message " + (index.getAndIncrement() + 1) + " of " + reportMessages.length);
                slackWebhookClient.postMessageToWebhook(message);
            }
        }).forEach(
                t -> scheduledExecutorService.schedule(t, 1, TimeUnit.SECONDS)
        );
        logger.info("All messages scheduled successfully");
```
created_at: 2025-04-26T21:45:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,25 +33,69 @@ public void run(
         logger.info("Java Timer trigger SlackReportingTimerTriggered executed at: " + LocalDateTime.now());
 
         String endpoint = getEnvVariable("ECOMMERCE_SLACK_REPORTING_WEBHOOK_ENDPOINT");
+        String reportStartDate = getEnvVariable("REPORT_START_DATE");
+        String reportEndDate = getEnvVariable("REPORT_END_DATE");
 
         SlackWebhookClient slackWebhookClient = createSlackWebhookClient(endpoint);
         LocalDate today = getCurrentDate();
-        LocalDate lastMonday = today.minusWeeks(1).with(DayOfWeek.MONDAY);
-        LocalDate lastSunday = lastMonday.with(DayOfWeek.SUNDAY);
+
+        LocalDate startDate = today.minusWeeks(1).with(DayOfWeek.MONDAY);
+        LocalDate endDate = startDate.with(DayOfWeek.SUNDAY);
+
+        if (reportStartDate != null && reportEndDate != null) {
+            startDate = getDateFromString(reportStartDate, startDate);
+            endDate = getDateFromString(reportEndDate, endDate);
+        }
 
         TransactionStatusAggregationService transactionStatusAggregationService = createAggregationService();
         List<AggregatedStatusGroup> aggregatedStatuses = transactionStatusAggregationService
-                .aggregateStatusCountByDateRange(lastMonday, lastSunday, logger);
+                .aggregateStatusCountByDateRange(startDate, endDate, logger);
 
         logger.info(
-                "Start date: " + lastMonday + " to date: " + lastSunday + ", results: " + aggregatedStatuses.size()
+                "Start date: " + startDate + " to date: " + endDate + ", results: " + aggregatedStatuses.size()
         );
 
-        // Create the report message
-        String report = SlackDateRangeReportMessageUtils
-                .createAggregatedWeeklyReport(aggregatedStatuses, lastMonday, lastSunday, logger);
-        // Send it to Slack
-        slackWebhookClient.postMessageToWebhook(report);
+        // Create the report messages
+        String[] reportMessages = SlackDateRangeReportMessageUtils
+                .createAggregatedWeeklyReport(aggregatedStatuses, startDate, endDate, logger);
+
+        // Send each message to Slack
+        logger.info("Sending " + reportMessages.length + " messages to Slack");
+        for (int i = 0; i < reportMessages.length; i++) {
+            logger.info("Sending message " + (i + 1) + " of " + reportMessages.length);
+            slackWebhookClient.postMessageToWebhook(reportMessages[i]);
+
+            // Add a small delay between messages to avoid rate limiting
+            if (i < reportMessages.length - 1) {
+                try {
+                    Thread.sleep(1000); // 1-second delay
+                } catch (InterruptedException e) {
+                    logger.warning("Sleep interrupted: " + e.getMessage());
+                    Thread.currentThread().interrupt();
+                }
+            }
+        }
+
+        logger.info("All messages sent successfully");
path: src/main/java/it/pagopa/ecommerce/reporting/functions/SlackReportingTimerTriggered.java
user: login: sfarralorenzo
body: Fixed here: 1c00493012539b47c5ce270f420a2206a8762b13 ✅ 
created_at: 2025-04-27T10:17:45Z
author_association: CONTRIBUTOR

PR comments:
user: login: sfarralorenzo
created_at: 2025-04-27T10:22:15Z
body: > Just an idea to open some discussions about the message format. The message now has the format of a list of all transaction status for each client/paymentMethod/psp. Since this format turned out to be too much long for a single slack message, in this PR we decided to split this message in more messages to show all info we retrieved. IMHO I think that the readability of multiple messages like these could be really conditioned by this format. So I'm thinking about two possible different format:
> 
> 1. a message format like the one implemented now, but without entry with 0 occurences for transaction status (not my preferred, but for sure very simple to implement)
> 2. a message format more compact that shows for each client/paymentMethod (no pspId you read rightly) status counter aggregated by notified_ok/notification_requested (ok status), notified_ko and other final status (ko status), and an aggregator counter for all non final statuses. We can consider to implement an httpTriggered Function to have a detailed status report for a predefined period to retrieve (if needed) more info about a specific date interval (and also client/paymentMethod/psp filters available).
> 
> What do you think?

The easiest thing to do in this PR was to remove the 0-occurrences statuses (done).
Anyway I agree that these messages are very hard to read, so a simplified approach is mandatory.

The idea of "no psp" is interesting.
We could also think to send a first message in the morning without the PSP "overhead", and another one later with the full report (the one we are sending right now).

Another idea could be to have N executions on the same day: the first one is focused on the NOTIFIED_X, the second one on the EXPIRED_X, and so on.

author_association: CONTRIBUTOR
user: login: sonarqubecloud[bot]
created_at: 2025-04-27T15:33:11Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=pagopa_pagopa-ecommerce-reporting-functions&pullRequest=12) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [26 New issues](https://sonarcloud.io/project/issues?id=pagopa_pagopa-ecommerce-reporting-functions&pullRequest=12&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=pagopa_pagopa-ecommerce-reporting-functions&pullRequest=12&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=pagopa_pagopa-ecommerce-reporting-functions&pullRequest=12&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [85.9% Coverage on New Code](https://sonarcloud.io/component_measures?id=pagopa_pagopa-ecommerce-reporting-functions&pullRequest=12&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=pagopa_pagopa-ecommerce-reporting-functions&pullRequest=12&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=pagopa_pagopa-ecommerce-reporting-functions&pullRequest=12)


author_association: NONE