feat : thêm bao cao

This commit is contained in:
nguyennt1 2025-12-03 23:27:42 +07:00
parent f9626aa701
commit 3c055be676
10 changed files with 664 additions and 0 deletions

View File

@ -2,6 +2,7 @@ package com.vega.hrm.core.repositories;
import com.vega.hrm.core.entities.RevenueData; import com.vega.hrm.core.entities.RevenueData;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.List;
import java.util.UUID; import java.util.UUID;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
@ -24,4 +25,13 @@ public interface RevenueDataRepository extends JpaRepository<RevenueData, UUID>
); );
RevenueData findByEmailAndRevenueDate(String email, LocalDate revenueDate); RevenueData findByEmailAndRevenueDate(String email, LocalDate revenueDate);
@Query("SELECT r FROM RevenueData r WHERE r.email = :email " +
"AND r.revenueDate >= :startDate AND r.revenueDate <= :endDate " +
"ORDER BY r.revenueDate ASC")
List<RevenueData> findAllByEmailAndDateRange(
@Param("email") String email,
@Param("startDate") LocalDate startDate,
@Param("endDate") LocalDate endDate
);
} }

View File

@ -6,10 +6,15 @@ import com.vega.hrm.core.component.TokenStore;
import com.vega.hrm.core.dto.GoogleOAuthConfig; import com.vega.hrm.core.dto.GoogleOAuthConfig;
import com.vega.hrm.core.models.responses.BaseResponse; import com.vega.hrm.core.models.responses.BaseResponse;
import com.vega.hrm.core.models.responses.PagedListResponse; import com.vega.hrm.core.models.responses.PagedListResponse;
import com.vega.hrm.report.request.ChannelReportRequest;
import com.vega.hrm.report.request.GetDragRevenueRequest; import com.vega.hrm.report.request.GetDragRevenueRequest;
import com.vega.hrm.report.request.GetRevenueRequest; import com.vega.hrm.report.request.GetRevenueRequest;
import com.vega.hrm.report.response.ChannelActivityReportDto;
import com.vega.hrm.report.response.ChannelRevenueReportDto;
import com.vega.hrm.report.response.ReportTypeDto; import com.vega.hrm.report.response.ReportTypeDto;
import com.vega.hrm.report.response.RevenueDataDto; import com.vega.hrm.report.response.RevenueDataDto;
import com.vega.hrm.report.serivce.ChannelActivityReportService;
import com.vega.hrm.report.serivce.ChannelRevenueReportService;
import com.vega.hrm.report.serivce.CreateReportingJobService; import com.vega.hrm.report.serivce.CreateReportingJobService;
import com.vega.hrm.report.serivce.RevenueDataService; import com.vega.hrm.report.serivce.RevenueDataService;
import com.vega.hrm.report.serivce.YouTubeReportService; import com.vega.hrm.report.serivce.YouTubeReportService;
@ -42,6 +47,8 @@ public class ReportGoogleController {
private final CreateReportingJobService createReportingJob; private final CreateReportingJobService createReportingJob;
private final YouTubeReportService youTubeReportService; private final YouTubeReportService youTubeReportService;
private final RevenueDataService revenueDataService; private final RevenueDataService revenueDataService;
private final ChannelRevenueReportService channelRevenueReportService;
private final ChannelActivityReportService channelActivityReportService;
private final GoogleOAuthConfig googleOAuthConfig; private final GoogleOAuthConfig googleOAuthConfig;
private final TokenStore tokenStore; private final TokenStore tokenStore;
@ -140,4 +147,68 @@ public class ReportGoogleController {
.build() .build()
); );
} }
@PostMapping("/channel/revenue")
@Operation(summary = "Báo cáo doanh thu theo kênh", description = "Tổng hợp báo cáo doanh thu theo kênh từ database với các thống kê và tổng hợp theo tuần/tháng.")
public ResponseEntity<BaseResponse<ChannelRevenueReportDto>> getChannelRevenueReport(
@Valid @RequestBody ChannelReportRequest request
) {
if (tokenStore.getTokenResponse(request.getEmail()) == null) {
return ResponseEntity.ok(
BaseResponse.<ChannelRevenueReportDto>builder()
.code("01")
.message("Không tìm thấy token Google cho email đã cung cấp")
.build()
);
}
ChannelRevenueReportDto report = channelRevenueReportService.generateRevenueReport(
request.getEmail(),
request.getStartDate(),
request.getEndDate(),
request.getIncludeWeeklySummary() != null && request.getIncludeWeeklySummary(),
request.getIncludeMonthlySummary() != null && request.getIncludeMonthlySummary()
);
return ResponseEntity.ok(
BaseResponse.<ChannelRevenueReportDto>builder()
.code("00")
.message("Lấy báo cáo doanh thu theo kênh thành công")
.data(report)
.build()
);
}
@PostMapping("/channel/activity")
@Operation(summary = "Báo cáo hoạt động theo kênh", description = "Lấy báo cáo hoạt động theo kênh từ YouTube Analytics API bao gồm views, watch time, subscribers, likes, comments.")
public ResponseEntity<BaseResponse<ChannelActivityReportDto>> getChannelActivityReport(
@Valid @RequestBody ChannelReportRequest request
) throws GeneralSecurityException, IOException {
if (tokenStore.getTokenResponse(request.getEmail()) == null) {
return ResponseEntity.ok(
BaseResponse.<ChannelActivityReportDto>builder()
.code("01")
.message("Không tìm thấy token Google cho email đã cung cấp")
.build()
);
}
ChannelActivityReportDto report = channelActivityReportService.getChannelActivityReport(
request.getEmail(),
request.getStartDate(),
request.getEndDate(),
GoogleNetHttpTransport.newTrustedTransport(),
JacksonFactory.getDefaultInstance(),
request.getIncludeWeeklySummary() != null && request.getIncludeWeeklySummary(),
request.getIncludeMonthlySummary() != null && request.getIncludeMonthlySummary()
);
return ResponseEntity.ok(
BaseResponse.<ChannelActivityReportDto>builder()
.code("00")
.message("Lấy báo cáo hoạt động theo kênh thành công")
.data(report)
.build()
);
}
} }

View File

@ -0,0 +1,23 @@
package com.vega.hrm.report.request;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class ChannelReportRequest {
@NotBlank(message = "Email không được để trống")
private String email;
@NotBlank(message = "Ngày bắt đầu không được để trống")
private String startDate; // format: YYYY-MM-DD
@NotBlank(message = "Ngày kết thúc không được để trống")
private String endDate; // format: YYYY-MM-DD
// Optional: Group by period
private Boolean includeWeeklySummary = false;
private Boolean includeMonthlySummary = false;
}

View File

@ -0,0 +1,21 @@
package com.vega.hrm.report.response;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ActivityDataDto {
private String date;
private Long views;
private Long watchTime; // seconds
private Long subscribers;
private Long likes;
private Long comments;
private Long shares;
}

View File

@ -0,0 +1,21 @@
package com.vega.hrm.report.response;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ActivitySummaryDto {
private String period; // "2025-W01" cho tuần, "2025-01" cho tháng
private Long totalViews;
private Long totalWatchTime;
private Long totalSubscribers;
private Long totalLikes;
private Long totalComments;
private Integer dayCount;
}

View File

@ -0,0 +1,40 @@
package com.vega.hrm.report.response;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ChannelActivityReportDto {
private String email;
private String startDate;
private String endDate;
// Tổng hợp metrics
private Long totalViews;
private Long totalWatchTime; // seconds
private Long totalSubscribers;
private Long totalLikes;
private Long totalComments;
// Trung bình
private Double averageViews;
private Double averageWatchTime;
private Double averageSubscribers;
// Thống
private Integer totalDays;
// Dữ liệu chi tiết theo ngày
private List<ActivityDataDto> dailyActivity;
// Tổng hợp theo tuần/tháng
private List<ActivitySummaryDto> weeklySummary;
private List<ActivitySummaryDto> monthlySummary;
}

View File

@ -0,0 +1,35 @@
package com.vega.hrm.report.response;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ChannelRevenueReportDto {
private String email;
private String startDate;
private String endDate;
// Tổng hợp doanh thu
private Double totalRevenue;
private Double averageDailyRevenue;
private Double maxDailyRevenue;
private Double minDailyRevenue;
// Thống
private Integer totalDays;
private Integer daysWithRevenue;
// Dữ liệu chi tiết theo ngày
private List<RevenueDataDto> dailyRevenue;
// Dữ liệu tổng hợp theo tuần/tháng (nếu cần)
private List<RevenueSummaryDto> weeklySummary;
private List<RevenueSummaryDto> monthlySummary;
}

View File

@ -0,0 +1,18 @@
package com.vega.hrm.report.response;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RevenueSummaryDto {
private String period; // "2025-W01" cho tuần, "2025-01" cho tháng
private Double totalRevenue;
private Double averageRevenue;
private Integer dayCount;
}

View File

@ -0,0 +1,245 @@
package com.vega.hrm.report.serivce;
import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.googleapis.json.GoogleJsonResponseException;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.services.youtubeAnalytics.v2.YouTubeAnalytics;
import com.google.api.services.youtubeAnalytics.v2.model.QueryResponse;
import com.vega.hrm.core.component.TokenStore;
import com.vega.hrm.core.dto.GoogleOAuthConfig;
import com.vega.hrm.core.helpers.LogHelper;
import com.vega.hrm.report.response.ActivityDataDto;
import com.vega.hrm.report.response.ActivitySummaryDto;
import com.vega.hrm.report.response.ChannelActivityReportDto;
import java.io.IOException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class ChannelActivityReportService {
private static final String APPLICATION_NAME = "vega-report";
private final TokenStore tokenStore;
private final GoogleOAuthConfig googleOAuthConfig;
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
/**
* Lấy báo cáo hoạt động theo kênh từ YouTube Analytics API
*/
public ChannelActivityReportDto getChannelActivityReport(String email, String startDate, String endDate,
HttpTransport httpTransport, JsonFactory jsonFactory,
boolean includeWeekly, boolean includeMonthly)
throws IOException {
Credential credential = tokenStore.buildCredential(
httpTransport,
jsonFactory,
email,
googleOAuthConfig.clientId,
googleOAuthConfig.clientSecret
);
if (credential == null) {
throw new IllegalStateException("Không tìm thấy token Google cho email: " + email);
}
YouTubeAnalytics youtubeAnalytics = new YouTubeAnalytics.Builder(httpTransport, jsonFactory, credential)
.setApplicationName(APPLICATION_NAME)
.build();
try {
// Lấy các metrics: views, watchTime, subscribers, likes, comments
QueryResponse queryResponse = youtubeAnalytics.reports().query()
.setIds("channel==MINE")
.setStartDate(startDate)
.setEndDate(endDate)
.setMetrics("views,estimatedMinutesWatched,subscribersGained,likes,comments")
.setDimensions("day")
.execute();
List<ActivityDataDto> dailyActivity = parseActivityData(queryResponse);
// Tính tổng hợp
long totalViews = dailyActivity.stream().mapToLong(a -> a.getViews() != null ? a.getViews() : 0L).sum();
long totalWatchTime = dailyActivity.stream().mapToLong(a -> a.getWatchTime() != null ? a.getWatchTime() : 0L).sum();
long totalSubscribers = dailyActivity.stream().mapToLong(a -> a.getSubscribers() != null ? a.getSubscribers() : 0L).sum();
long totalLikes = dailyActivity.stream().mapToLong(a -> a.getLikes() != null ? a.getLikes() : 0L).sum();
long totalComments = dailyActivity.stream().mapToLong(a -> a.getComments() != null ? a.getComments() : 0L).sum();
int totalDays = dailyActivity.size();
double avgViews = totalDays > 0 ? (double) totalViews / totalDays : 0.0;
double avgWatchTime = totalDays > 0 ? (double) totalWatchTime / totalDays : 0.0;
double avgSubscribers = totalDays > 0 ? (double) totalSubscribers / totalDays : 0.0;
ChannelActivityReportDto.ChannelActivityReportDtoBuilder reportBuilder = ChannelActivityReportDto.builder()
.email(email)
.startDate(startDate)
.endDate(endDate)
.totalViews(totalViews)
.totalWatchTime(totalWatchTime)
.totalSubscribers(totalSubscribers)
.totalLikes(totalLikes)
.totalComments(totalComments)
.averageViews(avgViews)
.averageWatchTime(avgWatchTime)
.averageSubscribers(avgSubscribers)
.totalDays(totalDays)
.dailyActivity(dailyActivity);
// Tổng hợp theo tuần
if (includeWeekly) {
reportBuilder.weeklySummary(generateWeeklySummary(dailyActivity));
} else {
reportBuilder.weeklySummary(new ArrayList<>());
}
// Tổng hợp theo tháng
if (includeMonthly) {
reportBuilder.monthlySummary(generateMonthlySummary(dailyActivity));
} else {
reportBuilder.monthlySummary(new ArrayList<>());
}
LogHelper.info("Đã lấy được " + dailyActivity.size() + " dòng dữ liệu hoạt động.");
return reportBuilder.build();
} catch (GoogleJsonResponseException e) {
LogHelper.error("Lỗi khi lấy dữ liệu hoạt động: " + e.getDetails().getMessage());
throw new IOException("Không thể lấy dữ liệu hoạt động: " + e.getDetails().getMessage());
}
}
/**
* Parse dữ liệu hoạt động từ QueryResponse
*/
private List<ActivityDataDto> parseActivityData(QueryResponse queryResponse) {
List<ActivityDataDto> result = new ArrayList<>();
if (queryResponse.getRows() != null) {
for (List<Object> row : queryResponse.getRows()) {
if (row.size() >= 6) {
String date = row.get(0).toString();
Long views = parseLong(row.get(1));
Long watchTime = parseLong(row.get(2)); // estimatedMinutesWatched (phút) -> chuyển sang giây
Long subscribers = parseLong(row.get(3));
Long likes = parseLong(row.get(4));
Long comments = parseLong(row.get(5));
// Chuyển watchTime từ phút sang giây
if (watchTime != null) {
watchTime = watchTime * 60;
}
result.add(ActivityDataDto.builder()
.date(date)
.views(views)
.watchTime(watchTime)
.subscribers(subscribers)
.likes(likes)
.comments(comments)
.build());
}
}
}
return result;
}
/**
* Parse giá trị Long từ Object
*/
private Long parseLong(Object obj) {
if (obj == null) {
return null;
}
if (obj instanceof Number) {
return ((Number) obj).longValue();
}
try {
return Long.parseLong(obj.toString());
} catch (NumberFormatException e) {
return null;
}
}
/**
* Tổng hợp dữ liệu theo tuần
*/
private List<ActivitySummaryDto> generateWeeklySummary(List<ActivityDataDto> data) {
Map<String, List<ActivityDataDto>> weeklyGrouped = data.stream()
.collect(Collectors.groupingBy(ad -> {
LocalDate date = LocalDate.parse(ad.getDate(), DATE_FORMATTER);
int year = date.getYear();
int week = date.get(java.time.temporal.WeekFields.ISO.weekOfWeekBasedYear());
return String.format("%d-W%02d", year, week);
}));
return weeklyGrouped.entrySet().stream()
.map(entry -> {
String period = entry.getKey();
List<ActivityDataDto> weekData = entry.getValue();
long totalViews = weekData.stream().mapToLong(a -> a.getViews() != null ? a.getViews() : 0L).sum();
long totalWatchTime = weekData.stream().mapToLong(a -> a.getWatchTime() != null ? a.getWatchTime() : 0L).sum();
long totalSubscribers = weekData.stream().mapToLong(a -> a.getSubscribers() != null ? a.getSubscribers() : 0L).sum();
long totalLikes = weekData.stream().mapToLong(a -> a.getLikes() != null ? a.getLikes() : 0L).sum();
long totalComments = weekData.stream().mapToLong(a -> a.getComments() != null ? a.getComments() : 0L).sum();
return ActivitySummaryDto.builder()
.period(period)
.totalViews(totalViews)
.totalWatchTime(totalWatchTime)
.totalSubscribers(totalSubscribers)
.totalLikes(totalLikes)
.totalComments(totalComments)
.dayCount(weekData.size())
.build();
})
.sorted((a, b) -> a.getPeriod().compareTo(b.getPeriod()))
.collect(Collectors.toList());
}
/**
* Tổng hợp dữ liệu theo tháng
*/
private List<ActivitySummaryDto> generateMonthlySummary(List<ActivityDataDto> data) {
Map<String, List<ActivityDataDto>> monthlyGrouped = data.stream()
.collect(Collectors.groupingBy(ad -> {
LocalDate date = LocalDate.parse(ad.getDate(), DATE_FORMATTER);
return String.format("%d-%02d", date.getYear(), date.getMonthValue());
}));
return monthlyGrouped.entrySet().stream()
.map(entry -> {
String period = entry.getKey();
List<ActivityDataDto> monthData = entry.getValue();
long totalViews = monthData.stream().mapToLong(a -> a.getViews() != null ? a.getViews() : 0L).sum();
long totalWatchTime = monthData.stream().mapToLong(a -> a.getWatchTime() != null ? a.getWatchTime() : 0L).sum();
long totalSubscribers = monthData.stream().mapToLong(a -> a.getSubscribers() != null ? a.getSubscribers() : 0L).sum();
long totalLikes = monthData.stream().mapToLong(a -> a.getLikes() != null ? a.getLikes() : 0L).sum();
long totalComments = monthData.stream().mapToLong(a -> a.getComments() != null ? a.getComments() : 0L).sum();
return ActivitySummaryDto.builder()
.period(period)
.totalViews(totalViews)
.totalWatchTime(totalWatchTime)
.totalSubscribers(totalSubscribers)
.totalLikes(totalLikes)
.totalComments(totalComments)
.dayCount(monthData.size())
.build();
})
.sorted((a, b) -> a.getPeriod().compareTo(b.getPeriod()))
.collect(Collectors.toList());
}
}

View File

@ -0,0 +1,180 @@
package com.vega.hrm.report.serivce;
import com.vega.hrm.core.entities.RevenueData;
import com.vega.hrm.core.repositories.RevenueDataRepository;
import com.vega.hrm.report.response.ChannelRevenueReportDto;
import com.vega.hrm.report.response.RevenueDataDto;
import com.vega.hrm.report.response.RevenueSummaryDto;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class ChannelRevenueReportService {
private final RevenueDataRepository revenueDataRepository;
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
/**
* Tạo báo cáo doanh thu theo kênh
*/
public ChannelRevenueReportDto generateRevenueReport(String email, String startDate, String endDate,
boolean includeWeekly, boolean includeMonthly) {
LocalDate start = LocalDate.parse(startDate, DATE_FORMATTER);
LocalDate end = LocalDate.parse(endDate, DATE_FORMATTER);
// Lấy tất cả dữ liệu trong khoảng thời gian
List<RevenueData> allData = revenueDataRepository.findAllByEmailAndDateRange(email, start, end);
if (allData == null || allData.isEmpty()) {
return ChannelRevenueReportDto.builder()
.email(email)
.startDate(startDate)
.endDate(endDate)
.totalRevenue(0.0)
.averageDailyRevenue(0.0)
.maxDailyRevenue(0.0)
.minDailyRevenue(0.0)
.totalDays(0)
.daysWithRevenue(0)
.dailyRevenue(new ArrayList<>())
.weeklySummary(new ArrayList<>())
.monthlySummary(new ArrayList<>())
.build();
}
// Chuyển đổi sang DTO
List<RevenueDataDto> dailyRevenue = allData.stream()
.map(this::convertToDto)
.collect(Collectors.toList());
// Tính tổng hợp
List<Double> revenues = allData.stream()
.filter(r -> r.getEstimatedRevenue() != null)
.map(RevenueData::getEstimatedRevenue)
.collect(Collectors.toList());
double totalRevenue = revenues.stream().mapToDouble(Double::doubleValue).sum();
double avgRevenue = revenues.isEmpty() ? 0.0 : totalRevenue / revenues.size();
double maxRevenue = revenues.stream().mapToDouble(Double::doubleValue).max().orElse(0.0);
double minRevenue = revenues.stream().mapToDouble(Double::doubleValue).min().orElse(0.0);
int totalDays = (int) start.datesUntil(end.plusDays(1)).count();
int daysWithRevenue = revenues.size();
ChannelRevenueReportDto.ChannelRevenueReportDtoBuilder reportBuilder = ChannelRevenueReportDto.builder()
.email(email)
.startDate(startDate)
.endDate(endDate)
.totalRevenue(totalRevenue)
.averageDailyRevenue(avgRevenue)
.maxDailyRevenue(maxRevenue)
.minDailyRevenue(minRevenue)
.totalDays(totalDays)
.daysWithRevenue(daysWithRevenue)
.dailyRevenue(dailyRevenue);
// Tổng hợp theo tuần
if (includeWeekly) {
reportBuilder.weeklySummary(generateWeeklySummary(allData));
} else {
reportBuilder.weeklySummary(new ArrayList<>());
}
// Tổng hợp theo tháng
if (includeMonthly) {
reportBuilder.monthlySummary(generateMonthlySummary(allData));
} else {
reportBuilder.monthlySummary(new ArrayList<>());
}
return reportBuilder.build();
}
/**
* Tổng hợp dữ liệu theo tuần
*/
private List<RevenueSummaryDto> generateWeeklySummary(List<RevenueData> data) {
Map<String, List<RevenueData>> weeklyGrouped = data.stream()
.collect(Collectors.groupingBy(rd -> {
LocalDate date = rd.getRevenueDate();
int year = date.getYear();
int week = date.get(java.time.temporal.WeekFields.ISO.weekOfWeekBasedYear());
return String.format("%d-W%02d", year, week);
}));
return weeklyGrouped.entrySet().stream()
.map(entry -> {
String period = entry.getKey();
List<RevenueData> weekData = entry.getValue();
List<Double> revenues = weekData.stream()
.filter(r -> r.getEstimatedRevenue() != null)
.map(RevenueData::getEstimatedRevenue)
.collect(Collectors.toList());
double total = revenues.stream().mapToDouble(Double::doubleValue).sum();
double avg = revenues.isEmpty() ? 0.0 : total / revenues.size();
return RevenueSummaryDto.builder()
.period(period)
.totalRevenue(total)
.averageRevenue(avg)
.dayCount(revenues.size())
.build();
})
.sorted((a, b) -> a.getPeriod().compareTo(b.getPeriod()))
.collect(Collectors.toList());
}
/**
* Tổng hợp dữ liệu theo tháng
*/
private List<RevenueSummaryDto> generateMonthlySummary(List<RevenueData> data) {
Map<String, List<RevenueData>> monthlyGrouped = data.stream()
.collect(Collectors.groupingBy(rd -> {
LocalDate date = rd.getRevenueDate();
return String.format("%d-%02d", date.getYear(), date.getMonthValue());
}));
return monthlyGrouped.entrySet().stream()
.map(entry -> {
String period = entry.getKey();
List<RevenueData> monthData = entry.getValue();
List<Double> revenues = monthData.stream()
.filter(r -> r.getEstimatedRevenue() != null)
.map(RevenueData::getEstimatedRevenue)
.collect(Collectors.toList());
double total = revenues.stream().mapToDouble(Double::doubleValue).sum();
double avg = revenues.isEmpty() ? 0.0 : total / revenues.size();
return RevenueSummaryDto.builder()
.period(period)
.totalRevenue(total)
.averageRevenue(avg)
.dayCount(revenues.size())
.build();
})
.sorted((a, b) -> a.getPeriod().compareTo(b.getPeriod()))
.collect(Collectors.toList());
}
/**
* Convert entity sang DTO
*/
private RevenueDataDto convertToDto(RevenueData entity) {
return RevenueDataDto.builder()
.date(entity.getRevenueDate().format(DATE_FORMATTER))
.estimatedRevenue(entity.getEstimatedRevenue())
.build();
}
}