HouseworkTaskRecalcService.java

package com.hwhub.batch.application.service;

import com.hwhub.batch.domain.enums.ProgramType;
import com.hwhub.batch.domain.enums.RecurrenceType;
import com.hwhub.batch.domain.model.Housework;
import com.hwhub.batch.domain.model.HouseworkTaskCreateParam;
import com.hwhub.batch.domain.model.HouseworkTaskRecalcRequest;
import com.hwhub.batch.domain.repository.HouseworkRepository;
import com.hwhub.batch.domain.repository.HouseworkTaskRecalcRequestRepository;
import com.hwhub.batch.domain.repository.HouseworkTaskRepository;
import com.hwhub.batch.domain.service.HouseworkScheduleCalculator;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.*;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class HouseworkTaskRecalcService {

  private static final Logger log = LoggerFactory.getLogger(HouseworkTaskRecalcService.class);

  private static final long SYSTEM_USER_ID = 2;

  private final HouseworkScheduleCalculator calculator;
  private final HouseworkTaskRecalcRequestRepository requestRepository;
  private final HouseworkTaskRepository taskRepository;
  private final HouseworkRepository houseworkRepository;

  /**
   * PENDING の houseworkId をまとめて再計算するエントリポイント。
   *
   * @param limit 1回の起動で処理する最大 housework 件数
   * @return 挿入/削除/更新されたタスク件数の合計
   */
  @Transactional
  public int recalcPending(int limit) {

    List<HouseworkTaskRecalcRequest> requests = requestRepository.findPendingRequests(limit);
    if (requests.isEmpty()) {
      log.info("No pending housework task recalc requests.");
      return 0;
    }

    log.info("Start recalc for {} houseworks: {}", requests.size(), requests);

    // houseworkId毎に集約
    Map<Long, List<Long>> houseworkId2RequestIds =
        requests.stream()
            .collect(
                Collectors.groupingBy(
                    HouseworkTaskRecalcRequest::houseworkId,
                    Collectors.mapping(
                        HouseworkTaskRecalcRequest::requestId, Collectors.toList())));

    int totalAffected = 0;
    for (Map.Entry<Long, List<Long>> entry : houseworkId2RequestIds.entrySet()) {
      long houseworkId = entry.getKey();
      List<Long> requestIds = entry.getValue();

      try {
        int affected = recalc4Housework(houseworkId);
        totalAffected += affected;
        requestRepository.markDoneByRequestIds(
            requestIds, SYSTEM_USER_ID, ProgramType.BTC_TSK_RECL.getCode());
      } catch (Exception e) {
        log.error("Failed to recalc housework tasks for houseworkId={}", houseworkId, e);
        requestRepository.markFailedByRequestIds(
            requestIds,
            Optional.ofNullable(e.getMessage()).orElse("unknown error"),
            SYSTEM_USER_ID,
            ProgramType.BTC_TSK_RECL.getCode());
      }
    }

    log.info("Finished recalc. totalAffected={}", totalAffected);
    return totalAffected;
  }

  /**
   * 家事マスタの定義の基づき、生成済みの家事タスクを再構成する。
   *
   * @param houseworkId 家事ID
   * @return 反映件数
   */
  private int recalc4Housework(long houseworkId) {
    LocalDate today = LocalDate.now(ZoneId.of("Asia/Tokyo"));

    Housework hw =
        houseworkRepository
            .findById(houseworkId)
            .orElseThrow(() -> new IllegalStateException("Housework not found: " + houseworkId));

    RecurrenceType type = RecurrenceType.fromCode(hw.getRecurrenceTypeCode());
    // タスクを生成する範囲
    LocalDate rangeTo =
        switch (type) {
          case WEEKLY -> today.plusDays(7);
          case MONTHLY, NTH_WEEKDAY -> today.plusMonths(1).plusDays(2);
        };

    LocalDate from = max(today, hw.getStartDate());
    LocalDate to = min(rangeTo, hw.getEndDate());
    if (from.isAfter(to)) {
      log.info("No target range for houseworkId={} (from={}, to={})", houseworkId, from, to);
      return 0;
    }

    List<LocalDate> expectedDates = calculator.calculateDates(hw, from, to);

    if (expectedDates.isEmpty()) {
      log.info("No expected dates for houseworkId={} in range {}..{}", houseworkId, from, to);
      return 0;
    }

    // 生成済みのタスクの日付を取得
    List<LocalDate> existingDates = taskRepository.findExistingTaskDates(houseworkId, today);

    Set<LocalDate> expectedSet = new HashSet<>(expectedDates);
    Set<LocalDate> existingSet = new HashSet<>(existingDates);

    // 新規作成する日付
    Set<LocalDate> toCreateDates = new HashSet<>(expectedSet);
    toCreateDates.removeAll(existingSet);

    // 削除する日付
    Set<LocalDate> toDeleteDates = new HashSet<>(existingSet);
    toDeleteDates.removeAll(expectedSet);

    // スナップショットを更新
    int snapshotUpdated =
        taskRepository.updateTaskSnapshot(
            houseworkId,
            from,
            hw.getName(),
            hw.getDescription(),
            hw.getCategory(),
            SYSTEM_USER_ID,
            ProgramType.BTC_TSK_RECL.getCode());

    // default担当者が変更されている場合、未対応で担当者の割当をしていないレコードを更新
    int assigneeUpdated = 0;
    if (hw.getDefaultAssigneeUserId() != null) {
      assigneeUpdated =
          taskRepository.updateAssigneeForSystemAssigned(
              houseworkId,
              from,
              hw.getDefaultAssigneeUserId(),
              SYSTEM_USER_ID,
              ProgramType.BTC_TSK_RECL.getCode());
    }

    // 削除
    int deleted = taskRepository.deleteUndoneTasksByDates(houseworkId, toDeleteDates);

    // 追加
    List<HouseworkTaskCreateParam> createParams =
        toCreateDates.stream()
            .map(
                date ->
                    HouseworkTaskCreateParam.from(
                        hw, date, SYSTEM_USER_ID, ProgramType.BTC_TSK_RECL.getCode()))
            .toList();
    int inserted = taskRepository.bulkInsertTasks(createParams);

    log.info(
        "Recalc houseworkId={} from={} to={}: snapshotUpdated={}, assigneeUpdated={}, deleted={}, inserted={}",
        houseworkId,
        from,
        to,
        snapshotUpdated,
        assigneeUpdated,
        deleted,
        inserted);

    return snapshotUpdated + assigneeUpdated + deleted + inserted;
  }

  /**
   * 指定された日付のうち大きい日付を返す。
   *
   * @param a 日付
   * @param b 日付
   * @return 指定された日付のうち大きい日付
   */
  private static LocalDate max(LocalDate a, LocalDate b) {
    return a.isAfter(b) ? a : b;
  }

  /**
   * 指定された日付のうち小さい日付を返す。
   *
   * @param a 日付
   * @param b 日付
   * @return 指定された日付のうち小さい日付
   */
  private static LocalDate min(LocalDate a, LocalDate b) {
    return a.isBefore(b) ? a : b;
  }
}