StaffSalesQueryServiceImpl.java

package com.deveagles.be15_deveagles_be.features.staffsales.query.service.impl;

import com.deveagles.be15_deveagles_be.features.sales.command.domain.aggregate.PaymentsMethod;
import com.deveagles.be15_deveagles_be.features.sales.command.domain.aggregate.SearchMode;
import com.deveagles.be15_deveagles_be.features.staffsales.command.domain.aggregate.ProductType;
import com.deveagles.be15_deveagles_be.features.staffsales.query.dto.request.GetStaffSalesListRequest;
import com.deveagles.be15_deveagles_be.features.staffsales.query.dto.response.*;
import com.deveagles.be15_deveagles_be.features.staffsales.query.repository.SalesTargetQueryRepository;
import com.deveagles.be15_deveagles_be.features.staffsales.query.repository.StaffSalesQueryRepository;
import com.deveagles.be15_deveagles_be.features.staffsales.query.service.StaffSalesQueryService;
import com.deveagles.be15_deveagles_be.features.staffsales.query.service.support.SalesCalculator;
import com.deveagles.be15_deveagles_be.features.users.command.domain.aggregate.Staff;
import com.deveagles.be15_deveagles_be.features.users.command.repository.UserRepository;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.time.temporal.ChronoUnit;
import java.util.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class StaffSalesQueryServiceImpl implements StaffSalesQueryService {

  private final UserRepository userRepository;
  private final SalesTargetQueryRepository salesTargetQueryRepository;
  private final StaffSalesQueryRepository staffSalesQueryRepository;
  private final SalesCalculator salesCalculator;

  @Override
  public StaffSalesListResult getStaffSales(Long shopId, GetStaffSalesListRequest request) {

    // 1. 조회 기간 계산
    LocalDateTime startDate = getStartDate(request);
    LocalDateTime endDate = getEndDate(request);

    // 2. 재직 중인 직원 리스트 조회
    List<Staff> staffList = userRepository.findByShopIdAndLeftDateIsNull(shopId);

    // 3. 직원별 매출 데이터 생성
    List<StaffSalesListResponse> result =
        staffList.stream()
            .map(
                staff -> {
                  List<StaffPaymentsSalesResponse> paymentsSalesList =
                      staffSalesQueryRepository
                          .getSalesByStaff(false, shopId, staff.getStaffId(), startDate, endDate)
                          .stream()
                          .map(
                              response -> {

                                // 인센티브율 맵 조회
                                ProductType type = ProductType.valueOf(response.getCategory());
                                Map<PaymentsMethod, Integer> incentiveRateMap =
                                    salesCalculator.getEffectiveIncentiveRates(
                                        shopId, staff.getStaffId(), type);

                                // 실매출 리스트 계산 (PREPAID 제외)
                                List<StaffNetSalesResponse> netSalesList =
                                    response.getNetSalesList().stream()
                                        .filter(
                                            net ->
                                                net.getPaymentsMethod()
                                                    != PaymentsMethod.PREPAID_PASS)
                                        .map(
                                            net -> {
                                              int amount =
                                                  Optional.ofNullable(net.getAmount()).orElse(0);
                                              int rate =
                                                  incentiveRateMap.getOrDefault(
                                                      net.getPaymentsMethod(), 0);
                                              int incentiveAmount =
                                                  (int) Math.floor(amount * (rate / 100.0));
                                              return StaffNetSalesResponse.builder()
                                                  .paymentsMethod(net.getPaymentsMethod())
                                                  .amount(amount)
                                                  .incentiveAmount(incentiveAmount)
                                                  .build();
                                            })
                                        .toList();

                                // 총 인센티브 합계
                                int incentiveTotal =
                                    netSalesList.stream()
                                        .mapToInt(
                                            net ->
                                                Optional.ofNullable(net.getIncentiveAmount())
                                                    .orElse(0))
                                        .sum();

                                // 총영업액 = 결제 금액 총합
                                int grossSalesTotal =
                                    netSalesList.stream()
                                        .mapToInt(
                                            net -> Optional.ofNullable(net.getAmount()).orElse(0))
                                        .sum();

                                // 공제액 계산
                                int deductionTotal =
                                    response.getDeductionList().stream()
                                        .mapToInt(d -> Optional.ofNullable(d.getAmount()).orElse(0))
                                        .sum();

                                // 실매출 = 총영업액 - 공제액
                                int netSalesTotal = grossSalesTotal - deductionTotal;

                                return StaffPaymentsSalesResponse.builder()
                                    .category(response.getCategory())
                                    .netSalesList(netSalesList)
                                    .deductionList(response.getDeductionList())
                                    .incentiveTotal(incentiveTotal)
                                    .grossSalesTotal(grossSalesTotal)
                                    .deductionTotal(deductionTotal)
                                    .netSalesTotal(netSalesTotal)
                                    .build();
                              })
                          .toList();

                  return StaffSalesListResponse.builder()
                      .staffId(staff.getStaffId())
                      .staffName(staff.getStaffName())
                      .paymentsSalesList(paymentsSalesList)
                      .build();
                })
            .toList();

    // 4. 전체 요약 계산
    StaffSalesSummaryResponse summary = salesCalculator.calculateSummary(shopId, result);

    return StaffSalesListResult.builder().staffSalesList(result).totalSummary(summary).build();
  }

  @Override
  public StaffSalesDetailListResult getStaffDetailSales(
      Long shopId, GetStaffSalesListRequest request) {

    // 1. 기간 계산
    LocalDateTime startDate = getStartDate(request);
    LocalDateTime endDate = getEndDate(request);

    // 2. 직원 리스트 조회
    List<Staff> staffList = userRepository.findByShopIdAndLeftDateIsNull(shopId);

    // 3. 직원별 매출 데이터 조회
    List<StaffDetailSalesListResponse> result =
        staffList.stream()
            .map(
                staff -> {
                  List<StaffPaymentsDetailSalesResponse> detailSales =
                      staffSalesQueryRepository.getDetailSalesByStaff(
                          staff.getStaffId(), shopId, startDate, endDate);

                  List<StaffPaymentsSalesResponse> sales =
                      staffSalesQueryRepository.getSalesByStaff(
                          true, shopId, staff.getStaffId(), startDate, endDate);

                  // 직원 요약 정보
                  StaffSalesSummaryResponse summary =
                      salesCalculator.calculateFromDetailAndSalesList(detailSales, sales);

                  return StaffDetailSalesListResponse.builder()
                      .staffId(staff.getStaffId())
                      .staffName(staff.getStaffName())
                      .paymentsSalesList(sales)
                      .paymentsDetailSalesList(detailSales)
                      .summary(summary)
                      .build();
                })
            .toList();

    StaffSalesSummaryResponse totalSummary =
        salesCalculator.calculateFromSummaryList(shopId, result);

    return StaffSalesDetailListResult.builder()
        .staffSalesList(result)
        .totalSummary(totalSummary)
        .build();
  }

  @Override
  public StaffSalesTargetListResult getStaffSalesTarget(
      Long shopId, GetStaffSalesListRequest request) {

    LocalDate startDate =
        request.searchMode() == SearchMode.MONTH
            ? request.startDate().withDayOfMonth(1)
            : request.startDate();

    LocalDate endDate =
        request.searchMode() == SearchMode.PERIOD
            ? request.endDate()
            : YearMonth.from(request.startDate()).atEndOfMonth();

    if (!hasAnyTargetForShop(shopId, startDate, endDate)) {
      return null;
    }

    List<Staff> staffList = userRepository.findAllByShopId(shopId);

    List<StaffSalesTargetResponse> staffResponse =
        staffList.stream()
            .map(
                staff -> {
                  Long staffId = staff.getStaffId();

                  List<StaffProductTargetSalesResponse> targetList =
                      List.of(
                          buildCombinedTargetResponse(
                              shopId, staffId, true, "상품", startDate, endDate, request),
                          buildCombinedTargetResponse(
                              shopId, staffId, false, "회원권", startDate, endDate, request));

                  int totalTarget =
                      targetList.stream()
                          .mapToInt(StaffProductTargetSalesResponse::getTargetAmount)
                          .sum();
                  int totalActual =
                      targetList.stream()
                          .mapToInt(StaffProductTargetSalesResponse::getTotalAmount)
                          .sum();
                  double totalRate =
                      salesCalculator.calculateAchievementRate(totalActual, totalTarget);

                  return StaffSalesTargetResponse.builder()
                      .staffId(staffId)
                      .staffName(staff.getStaffName())
                      .targetSalesList(targetList)
                      .totalTargetAmount(totalTarget)
                      .totalActualAmount(totalActual)
                      .totalAchievementRate(totalRate)
                      .build();
                })
            .toList();

    return StaffSalesTargetListResult.builder().staffSalesList(staffResponse).build();
  }

  private StaffProductTargetSalesResponse buildCombinedTargetResponse(
      Long shopId,
      Long staffId,
      boolean isItems,
      String label,
      LocalDate startDate,
      LocalDate endDate,
      GetStaffSalesListRequest request) {

    List<YearMonth> months = getYearMonthsBetween(startDate, endDate);
    int totalAdjustedTarget = 0;

    for (YearMonth ym : months) {
      LocalDate monthStart = ym.atDay(1);
      LocalDate monthEnd = ym.atEndOfMonth();

      LocalDate overlapStart = startDate.isAfter(monthStart) ? startDate : monthStart;
      LocalDate overlapEnd = endDate.isBefore(monthEnd) ? endDate : monthEnd;
      int includedDays = (int) ChronoUnit.DAYS.between(overlapStart, overlapEnd) + 1;

      int monthlyTarget =
          salesTargetQueryRepository.findTargetAmountByItemsOrMembership(
              shopId, staffId, isItems, ym);

      int adjustedTarget =
          salesCalculator.calculateAdjustedTarget(
              request.searchMode(), monthlyTarget, ym.lengthOfMonth(), includedDays);

      totalAdjustedTarget += adjustedTarget;
    }

    ProductType type1 = isItems ? ProductType.SERVICE : ProductType.SESSION_PASS;
    ProductType type2 = isItems ? ProductType.PRODUCT : ProductType.PREPAID_PASS;

    int actualSales1 =
        staffSalesQueryRepository.getTargetTotalSales(shopId, staffId, type1, startDate, endDate);
    int actualSales2 =
        staffSalesQueryRepository.getTargetTotalSales(shopId, staffId, type2, startDate, endDate);
    int totalActualSales = actualSales1 + actualSales2;

    double achievement =
        salesCalculator.calculateAchievementRate(totalActualSales, totalAdjustedTarget);

    return StaffProductTargetSalesResponse.builder()
        .label(label)
        .targetAmount(totalAdjustedTarget)
        .totalAmount(totalActualSales)
        .achievementRate(achievement)
        .build();
  }

  private List<YearMonth> getYearMonthsBetween(LocalDate startDate, LocalDate endDate) {
    List<YearMonth> months = new ArrayList<>();
    YearMonth current = YearMonth.from(startDate);
    YearMonth last = YearMonth.from(endDate);

    while (!current.isAfter(last)) {
      months.add(current);
      current = current.plusMonths(1);
    }

    return months;
  }

  private LocalDateTime getStartDate(GetStaffSalesListRequest request) {
    if (request.searchMode() == SearchMode.MONTH) {
      return request.startDate().withDayOfMonth(1).atStartOfDay();
    }
    return request.startDate().atStartOfDay();
  }

  private LocalDateTime getEndDate(GetStaffSalesListRequest request) {
    LocalDate endDate =
        getEffectiveEndDate(request.startDate(), request.endDate(), request.searchMode());
    return endDate.atTime(23, 59, 59);
  }

  private LocalDate getEffectiveEndDate(LocalDate startDate, LocalDate endDate, SearchMode mode) {
    if (mode == SearchMode.MONTH) {
      return startDate.withDayOfMonth(startDate.lengthOfMonth());
    }
    return Optional.ofNullable(endDate).orElse(startDate);
  }

  private int getPeriodDays(LocalDate start, LocalDate end) {
    return (int) ChronoUnit.DAYS.between(start, end) + 1;
  }

  private boolean hasAnyTargetForShop(Long shopId, LocalDate startDate, LocalDate endDate) {
    List<YearMonth> yearMonthList = getYearMonthsInRange(startDate, endDate);
    return salesTargetQueryRepository.existsTargetForShopInMonths(shopId, yearMonthList);
  }

  private List<YearMonth> getYearMonthsInRange(LocalDate startDate, LocalDate endDate) {
    List<YearMonth> result = new ArrayList<>();
    YearMonth start = YearMonth.from(startDate);
    YearMonth end = YearMonth.from(endDate);

    while (!start.isAfter(end)) {
      result.add(start);
      start = start.plusMonths(1);
    }

    return result;
  }
}