ShoppingItemService.java

package com.hwhub.backend.application.service;

import com.hwhub.backend.domain.enums.FavoriteFlag;
import com.hwhub.backend.domain.enums.ProgramType;
import com.hwhub.backend.domain.enums.ShoppingItemStatus;
import com.hwhub.backend.domain.model.ShoppingItemAttachment;
import com.hwhub.backend.domain.model.ShoppingItemHistorySuggestionModel;
import com.hwhub.backend.domain.model.ShoppingItemModel;
import com.hwhub.backend.domain.repository.ShoppingItemAttachmentRepository;
import com.hwhub.backend.domain.repository.ShoppingItemRepository;
import com.hwhub.backend.presentation.rest.common.ResourceNotFoundException;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class ShoppingItemService {

  private final ShoppingItemRepository shoppingItemRepository;
  private final HouseholdAuthorizationService householdAuthorizationService;
  private final ShoppingItemAttachmentRepository shoppingItemAttachmentRepository;

  /** 世帯の買い物アイテム一覧を取得する */
  @Transactional(readOnly = true)
  public List<ShoppingItemModel> getShoppingItems(long householdId, long userId) {

    if (!householdAuthorizationService.canAccessHousehold(householdId, userId)) {
      throw new AccessDeniedException(
          "User does not belong to household: userId=" + userId + ", householdId=" + householdId);
    }

    return shoppingItemRepository.findByHouseholdId(householdId);
  }

  /** 世帯のお気に入り買い物アイテム一覧を取得する */
  @Transactional(readOnly = true)
  public List<ShoppingItemModel> getFavoriteShoppingItems(long householdId, long userId) {

    if (!householdAuthorizationService.canAccessHousehold(householdId, userId)) {
      throw new AccessDeniedException(
          "User does not belong to household: userId=" + userId + ", householdId=" + householdId);
    }

    return shoppingItemRepository.findFavoritesByHouseholdId(householdId);
  }

  public void updateFavorite(long shoppingItemId, String favorite, long userId) {

    var opt = shoppingItemRepository.findById(shoppingItemId);
    if (opt.isEmpty()) {
      throw new ResourceNotFoundException("SHOPPING_ITEM_NOT_FOUND");
    }
    var item = opt.get();

    // 認可チェック
    if (!householdAuthorizationService.canAccessHousehold(item.getHouseholdId(), userId)) {
      throw new AccessDeniedException("You do not have access to this household.");
    }

    if (FavoriteFlag.FAVORITE.getCode().equals(favorite)) {
      item.favorite();
    } else {
      item.clearFavorite();
    }

    shoppingItemRepository.update(item, userId, ProgramType.ONL_SHP.getCode());
  }

  public void updateStatus(long shoppingItemId, String status, long userId) {

    var itemOpt = shoppingItemRepository.findById(shoppingItemId);
    if (itemOpt.isEmpty()) {
      throw new ResourceNotFoundException("SHOPPING_ITEM_NOT_FOUND");
    }
    var item = itemOpt.get();

    if (!householdAuthorizationService.canAccessHousehold(item.getHouseholdId(), userId)) {
      throw new AccessDeniedException("You do not have access to this household.");
    }

    ShoppingItemStatus shoppingItemStatus = ShoppingItemStatus.fromCode(status);
    switch (shoppingItemStatus) {
      case NOT_PURCHASED -> item.notPurcased();
      case PURCHASED -> item.purchased();
      default -> item.inBasket();
    }

    shoppingItemRepository.update(item, userId, ProgramType.ONL_SHP.getCode());
  }

  @Transactional
  public ShoppingItemModel create(ShoppingItemModel model, Long sourceShoppingItemId, long userId) {

    // 認可チェック
    if (!householdAuthorizationService.canAccessHousehold(model.getHouseholdId(), userId)) {
      throw new AccessDeniedException("You do not have access to this household.");
    }

    ShoppingItemModel saved =
        shoppingItemRepository.insert(model, userId, ProgramType.ONL_SHP.getCode());

    if (sourceShoppingItemId == null) {
      return saved;
    }

    var sourceItemOpt = shoppingItemRepository.findById(sourceShoppingItemId);
    if (sourceItemOpt.isEmpty()) {
      // 見つからなくても、本体は作れているのでそのまま返す
      return saved;
    }
    var sourceItem = sourceItemOpt.get();

    if (!sourceItem.getHouseholdId().equals(saved.getHouseholdId())) {
      // household が異なるitemをコピーしようとしていたら無視
      return saved;
    }

    var sourceAttachments =
        shoppingItemAttachmentRepository.findByShoppingItemId(sourceShoppingItemId);

    int nextSortOrder = 1;
    for (ShoppingItemAttachment src : sourceAttachments) {
      ShoppingItemAttachment atttach =
          ShoppingItemAttachment.create(
              saved.getShoppingItemId(),
              src.getFileKey(),
              src.getFileName(),
              src.getMimeType(),
              nextSortOrder++);
      shoppingItemAttachmentRepository.save(atttach, userId, ProgramType.ONL_SHP.getCode());
    }
    return saved;
  }

  @Transactional
  public ShoppingItemModel update(
      Long shoppingItemId, String name, String memo, String storeType, Long userId) {

    ShoppingItemModel model =
        shoppingItemRepository
            .findById(shoppingItemId)
            .orElseThrow(() -> new ResourceNotFoundException("SHOPPING_ITEM_NOT_FOUND"));

    // 認可チェック
    if (!householdAuthorizationService.canAccessHousehold(model.getHouseholdId(), userId)) {
      throw new AccessDeniedException("You do not have access to this household.");
    }

    model.update(name, memo, storeType);
    return shoppingItemRepository.update(model, userId, ProgramType.ONL_SHP.getCode());
  }

  @Transactional
  public void bulkUpdateStatus(List<Long> ids, String status, long userId) {
    ShoppingItemStatus.fromCode(status); // バリデーション(不正コードは例外)

    // IN句で一括取得(N+1解消)
    List<ShoppingItemModel> items = shoppingItemRepository.findByIds(ids);

    if (items.size() != ids.size()) {
      throw new ResourceNotFoundException("SHOPPING_ITEM_NOT_FOUND");
    }

    // 認可チェック(一括取得した全アイテムに対して実施)
    for (ShoppingItemModel item : items) {
      if (!householdAuthorizationService.canAccessHousehold(item.getHouseholdId(), userId)) {
        throw new AccessDeniedException("You do not have access to this household.");
      }
    }

    // IN句で一括更新(N+1解消)
    shoppingItemRepository.bulkUpdateStatus(ids, status, userId, ProgramType.ONL_SHP.getCode());
  }

  @Transactional
  public void delete(long shoppingItemId, long userId) {
    var itemOpt = shoppingItemRepository.findById(shoppingItemId);
    if (itemOpt.isEmpty()) {
      throw new ResourceNotFoundException("SHOPPING_ITEM_NOT_FOUND");
    }
    var item = itemOpt.get();

    if (!householdAuthorizationService.canAccessHousehold(item.getHouseholdId(), userId)) {
      throw new AccessDeniedException("You do not have access to this household.");
    }

    // 添付画像を一括削除
    shoppingItemAttachmentRepository.deleteByShoppingItemId(shoppingItemId);

    shoppingItemRepository.deleteById(shoppingItemId);
  }

  @Transactional(readOnly = true)
  public List<ShoppingItemHistorySuggestionModel> listHistorySuggestions(
      Long householdId, Long userId, String keyword, String storeType, int limit) {
    // household 認可チェック
    householdAuthorizationService.assertUserBelongsToHousehold(householdId, userId);

    int actualLimit = (limit <= 0 || limit > 100) ? 20 : limit;

    return shoppingItemRepository.findHistorySuggestions(
        householdId, keyword, storeType, actualLimit);
  }
}