Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
package com.bbangle.bbangle.claim.domain;

import static com.bbangle.bbangle.claim.domain.constant.ExchangeRequestStatus.APPROVED;
import static com.bbangle.bbangle.claim.domain.constant.ExchangeRequestStatus.REJECTED;
import static com.bbangle.bbangle.claim.domain.constant.ExchangeRequestStatus.REQUESTED;

import com.bbangle.bbangle.claim.domain.constant.ExchangeRequestStatus;
import com.bbangle.bbangle.exception.BbangleErrorCode;
import com.bbangle.bbangle.exception.BbangleException;
import com.bbangle.bbangle.order.domain.OrderItem;
import jakarta.persistence.Column;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

Expand All @@ -22,4 +31,36 @@ public class ExchangeRequest extends Claim {
@Enumerated(EnumType.STRING)
private ExchangeRequestStatus status;

private String sellerComment;

@Builder
public ExchangeRequest(
OrderItem orderItem,
String detailReason,
LocalDateTime decidedAt,
ExchangeRequestStatus status,
String sellerComment
) {
super(orderItem, detailReason, decidedAt);
this.status = status;
this.sellerComment = sellerComment;
}
Comment on lines +34 to +47
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find SQL migration files to check seller_comment column definition
fd -e sql -type f | head -20

Repository: eco-dessert-platform/backend

Length of output: 242


🏁 Script executed:

# Search for seller_comment references in SQL files
rg -i 'seller_comment' --type sql

Repository: eco-dessert-platform/backend

Length of output: 178


🏁 Script executed:

# Examine ExchangeRequest.java to see the actual field and parent class
cat -n src/main/java/com/bbangle/bbangle/claim/domain/ExchangeRequest.java

Repository: eco-dessert-platform/backend

Length of output: 2675


🏁 Script executed:

# Check the parent Claim class
fd -e java | xargs rg -l 'class Claim' | head -5

Repository: eco-dessert-platform/backend

Length of output: 430


🏁 Script executed:

# Check ReturnRequest.java for comparison
cat -n src/main/java/com/bbangle/bbangle/claim/domain/ReturnRequest.java

Repository: eco-dessert-platform/backend

Length of output: 2697


🏁 Script executed:

# Check the parent Claim class structure
cat -n src/main/java/com/bbangle/bbangle/claim/domain/Claim.java | head -100

Repository: eco-dessert-platform/backend

Length of output: 1919


🏁 Script executed:

# Check the database migration to see full context of seller_comment column
cat -n src/main/resources/flyway/V33__app.sql | grep -A5 -B5 'seller_comment'

Repository: eco-dessert-platform/backend

Length of output: 385


🏁 Script executed:

# Search for exchange_request table definition and seller_comment column in all migrations
rg -i 'exchange_request' src/main/resources/flyway/ -A10 -B2 | grep -i 'seller_comment\|alter table\|create table'

Repository: eco-dessert-platform/backend

Length of output: 204


🏁 Script executed:

# Check all flyway migrations for exchange_request table creation
fd sql -x grep -l 'exchange_request' src/main/resources/flyway/

Repository: eco-dessert-platform/backend

Length of output: 1942


🏁 Script executed:

# Check V21__app.sql for exchange_request table definition and seller_comment
cat -n src/main/resources/flyway/V21__app.sql | grep -A30 'CREATE TABLE exchange_request'

Repository: eco-dessert-platform/backend

Length of output: 1407


🏁 Script executed:

# Check all flyway migrations in order to see if seller_comment was added to exchange_request after V33
ls -1 src/main/resources/flyway/ | sort -V

Repository: eco-dessert-platform/backend

Length of output: 527


🏁 Script executed:

# Search all migration files for any reference to exchange_request and seller_comment
rg 'exchange_request' src/main/resources/flyway/ -A20

Repository: eco-dessert-platform/backend

Length of output: 1790


🏁 Script executed:

# Check V34 and later migrations for any changes to exchange_request
cat src/main/resources/flyway/V34__app.sql | grep -i 'exchange_request\|seller_comment'

Repository: eco-dessert-platform/backend

Length of output: 54


🏁 Script executed:

# Check if there are any other migrations that modify exchange_request
for file in src/main/resources/flyway/V3{4,5,6,7}__app.sql; do
  echo "=== $file ==="
  grep -i 'exchange_request\|alter table exchange' "$file" || echo "No matches"
done

Repository: eco-dessert-platform/backend

Length of output: 392


🏁 Script executed:

# Check if there's a migration that adds seller_comment to exchange_request that we might have missed
rg 'ALTER TABLE exchange_request' src/main/resources/flyway/

Repository: eco-dessert-platform/backend

Length of output: 54


🏁 Script executed:

# List all migrations and check content of files after V33
for file in src/main/resources/flyway/V36__app.sql src/main/resources/flyway/V37__app.sql; do
  [ -f "$file" ] && echo "=== $file ===" && head -100 "$file"
done

Repository: eco-dessert-platform/backend

Length of output: 1816


exchange_request 테이블에 seller_comment 컬럼이 누락되어 있습니다.

ExchangeRequest.javasellerComment 필드(34줄)가 코드에서 사용 중이지만(approve/reject 메서드), 데이터베이스 스키마에서 해당 컬럼이 없습니다. V21에서 exchange_request 테이블이 생성될 때 status 컬럼만 포함되었으며, V33에서는 seller_commentreturn_request 테이블에만 추가되었습니다. 이로 인해 엔티티 저장 시 컬럼 불일치 오류가 발생합니다.

Flyway 마이그레이션 파일에 다음을 추가하여 exchange_request 테이블에 seller_comment 컬럼을 추가해야 합니다:

ALTER TABLE exchange_request
    ADD COLUMN seller_comment VARCHAR(255) COMMENT '판매자의 승인/거절 사유';

또한 sellerComment 필드에 @Column(name = "seller_comment") 어노테이션을 명시적으로 추가하는 것이 권장됩니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/bbangle/bbangle/claim/domain/ExchangeRequest.java` around
lines 34 - 47, The entity ExchangeRequest has a sellerComment field used by
approve/reject but the exchange_request table lacks the seller_comment column;
add a Flyway migration that runs "ALTER TABLE exchange_request ADD COLUMN
seller_comment VARCHAR(255) COMMENT '판매자의 승인/거절 사유';" (new V migration file) and
update the ExchangeRequest class to annotate the field with `@Column`(name =
"seller_comment") on the sellerComment property so the JPA mapping matches the
DB; ensure the migration is applied before deploying changes that call
ExchangeRequest.approve/reject.


public void approve(String reason) {
if (status != REQUESTED) {
throw new BbangleException(BbangleErrorCode.CLAIM_INVALID_STATUS);
}
this.status = APPROVED;
this.sellerComment = reason;
super.decide();
}

public void reject(String reason) {
if (status != REQUESTED) {
throw new BbangleException(BbangleErrorCode.CLAIM_INVALID_STATUS);
}
this.status = REJECTED;
this.sellerComment = reason;
super.decide();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,12 @@ public ReturnRequest(
OrderItem orderItem,
String detailReason,
LocalDateTime decidedAt,
ReturnRequestRequestStatus status
ReturnRequestRequestStatus status,
String sellerComment
) {
super(orderItem, detailReason, decidedAt);
this.status = status;
this.sellerComment = sellerComment;
}

public void approve(String reason) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.bbangle.bbangle.claim.repository;

import com.bbangle.bbangle.claim.domain.ExchangeRequest;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ExchangeRequestRepository extends JpaRepository<ExchangeRequest, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package com.bbangle.bbangle.claim.seller.service;

import com.bbangle.bbangle.claim.domain.ExchangeRequest;
import com.bbangle.bbangle.claim.domain.constant.ExchangeRequestStatus;
import com.bbangle.bbangle.claim.repository.ExchangeRequestRepository;
import com.bbangle.bbangle.claim.seller.service.model.ExchangeCreateCommand;
import com.bbangle.bbangle.exception.BbangleErrorCode;
import com.bbangle.bbangle.exception.BbangleException;
import com.bbangle.bbangle.order.domain.Order;
import com.bbangle.bbangle.order.domain.OrderItem;
import com.bbangle.bbangle.order.domain.OrderItemHistory;
import com.bbangle.bbangle.order.repository.OrderItemHistoryRepository;
import com.bbangle.bbangle.order.repository.OrderItemRepository;
import com.bbangle.bbangle.order.repository.OrderRepository;
import com.bbangle.bbangle.order.seller.controller.dto.response.SellerOrderResponse;
import com.bbangle.bbangle.order.seller.controller.dto.response.SellerOrderResponse.ExchangeContent;
import com.bbangle.bbangle.order.seller.controller.dto.response.SellerOrderResponse.ExchangeCreateResponse;
import com.bbangle.bbangle.seller.repository.SellerRepository;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@RequiredArgsConstructor
@Service
public class SellerExchangeService {

private final ExchangeRequestRepository exchangeRequestRepository;
private final OrderItemHistoryRepository orderItemHistoryRepository;
private final OrderRepository orderRepository;
private final OrderItemRepository orderItemRepository;
private final SellerRepository sellerRepository;

@Transactional
public ExchangeCreateResponse createExchange(ExchangeCreateCommand command) {
if (command.orderItemIds() == null || command.orderItemIds().isEmpty()) {
throw new BbangleException(BbangleErrorCode.ORDER_ITEM_NOT_FOUND);
}

List<Long> uniqueOrderItemIds = command.orderItemIds().stream()
.distinct()
.toList();

int requestedCount = uniqueOrderItemIds.size();

Order order = orderRepository.findById(command.orderId())
.orElseThrow(() -> new BbangleException(BbangleErrorCode.ORDER_NOT_FOUND));

Long storeId = getStoreIdOrThrow(command.sellerId());

List<OrderItem> orderItems = orderItemRepository.findByOrderIdAndIdIn(
order.getId(),
uniqueOrderItemIds
);

Set<Long> foundIds = orderItems.stream()
.map(OrderItem::getId)
.collect(Collectors.toSet());

if (!foundIds.isEmpty()) {
assertOwnedOrderItems(order.getId(), new ArrayList<>(foundIds), storeId);
}

List<Long> notFoundIds = uniqueOrderItemIds.stream()
.filter(id -> !foundIds.contains(id))
.toList();

List<Long> successOrderItemIds = new ArrayList<>();
List<Long> failedOrderItemIds = new ArrayList<>(notFoundIds);
List<ExchangeRequest> exchangeRequestsToSave = new ArrayList<>();
List<OrderItemHistory> historiesToSave = new ArrayList<>();

for (OrderItem orderItem : orderItems) {
if (orderItem.requestExchange()) {
ExchangeRequest exchangeRequest = ExchangeRequest.builder()
.orderItem(orderItem)
.detailReason(command.reason())
.sellerComment(command.sellerComment())
.status(ExchangeRequestStatus.REQUESTED)
.build();
exchangeRequestsToSave.add(exchangeRequest);
historiesToSave.add(OrderItemHistory.create(orderItem));
successOrderItemIds.add(orderItem.getId());
} else {
failedOrderItemIds.add(orderItem.getId());
}
}

exchangeRequestRepository.saveAll(exchangeRequestsToSave);
orderItemHistoryRepository.saveAll(historiesToSave);

int successCount = successOrderItemIds.size();
int failCount = failedOrderItemIds.size();

SellerOrderResponse.Summary summary =
SellerOrderResponse.Summary.of(requestedCount, successCount, failCount);

ExchangeContent content = ExchangeContent.of(
order.getId(),
summary,
successOrderItemIds,
failedOrderItemIds
);

return ExchangeCreateResponse.of(content);
}

private Long getStoreIdOrThrow(Long sellerId) {
Long storeId = sellerRepository.findStoreIdBySellerId(sellerId);
if (storeId == null) {
throw new BbangleException(BbangleErrorCode.SELLER_NOT_FOUND);
}
return storeId;
}

private void assertOwnedOrderItems(Long orderId, List<Long> orderItemIds, Long storeId) {
long ownedCount = orderItemRepository.countOwnedOrderItems(orderId, orderItemIds, storeId);
if (ownedCount != orderItemIds.size()) {
throw new BbangleException(BbangleErrorCode.ORDER_ACCESS_DENIED);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,25 @@

import com.bbangle.bbangle.claim.domain.ReturnRequest;
import com.bbangle.bbangle.claim.domain.constant.DecisionType;
import com.bbangle.bbangle.claim.domain.constant.ReturnRequestRequestStatus;
import com.bbangle.bbangle.claim.repository.ReturnRequestRepository;
import com.bbangle.bbangle.claim.seller.service.model.ReturnCreateCommand;
import com.bbangle.bbangle.exception.BbangleErrorCode;
import com.bbangle.bbangle.exception.BbangleException;
import com.bbangle.bbangle.order.domain.Order;
import com.bbangle.bbangle.order.domain.OrderItem;
import com.bbangle.bbangle.order.domain.OrderItemHistory;
import com.bbangle.bbangle.order.repository.OrderItemHistoryRepository;
import com.bbangle.bbangle.order.repository.OrderItemRepository;
import com.bbangle.bbangle.order.repository.OrderRepository;
import com.bbangle.bbangle.order.seller.controller.dto.response.SellerOrderResponse;
import com.bbangle.bbangle.order.seller.controller.dto.response.SellerOrderResponse.ReturnContent;
import com.bbangle.bbangle.order.seller.controller.dto.response.SellerOrderResponse.ReturnCreateResponse;
import com.bbangle.bbangle.seller.repository.SellerRepository;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -19,6 +31,98 @@ public class SellerReturnService {

private final ReturnRequestRepository returnRequestRepository;
private final OrderItemHistoryRepository orderItemHistoryRepository;
private final OrderRepository orderRepository;
private final OrderItemRepository orderItemRepository;
private final SellerRepository sellerRepository;

@Transactional
public ReturnCreateResponse createReturn(ReturnCreateCommand command) {
if (command.orderItemIds() == null || command.orderItemIds().isEmpty()) {
throw new BbangleException(BbangleErrorCode.ORDER_ITEM_NOT_FOUND);
}

List<Long> uniqueOrderItemIds = command.orderItemIds().stream()
.distinct()
.toList();

int requestedCount = uniqueOrderItemIds.size();

Order order = orderRepository.findById(command.orderId())
.orElseThrow(() -> new BbangleException(BbangleErrorCode.ORDER_NOT_FOUND));

Long storeId = getStoreIdOrThrow(command.sellerId());

List<OrderItem> orderItems = orderItemRepository.findByOrderIdAndIdIn(
order.getId(),
uniqueOrderItemIds
);

Set<Long> foundIds = orderItems.stream()
.map(OrderItem::getId)
.collect(Collectors.toSet());

if (!foundIds.isEmpty()) {
assertOwnedOrderItems(order.getId(), new ArrayList<>(foundIds), storeId);
}
Comment on lines +63 to +66
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

foundIds가 비어있을 때 소유권 검증이 건너뛰어집니다.

요청된 orderItemIds가 모두 해당 주문에 존재하지 않는 경우, 소유권 검증 없이 모든 아이템이 실패로 처리됩니다. 기능적으로 변경이 발생하지 않아 큰 문제는 아니지만, 판매자가 자신의 스토어 소유가 아닌 주문에 대해 아이템 ID 존재 여부를 간접적으로 확인할 수 있는 여지가 있습니다.

foundIds가 비어있더라도 일관성을 위해 early return하거나, 먼저 주문 자체의 소유권을 검증하는 것을 고려해 주세요.

🛡️ 제안: 아이템이 없을 때 early return
         if (!foundIds.isEmpty()) {
             assertOwnedOrderItems(order.getId(), new ArrayList<>(foundIds), storeId);
+        } else {
+            return ReturnCreateResponse.of(ReturnContent.of(
+                order.getId(),
+                SellerOrderResponse.Summary.of(requestedCount, 0, requestedCount),
+                List.of(),
+                new ArrayList<>(uniqueOrderItemIds)
+            ));
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/bbangle/bbangle/claim/seller/service/SellerReturnService.java`
around lines 63 - 66, The code skips ownership verification when foundIds is
empty, allowing a caller to infer item existence; in SellerReturnService update
the logic so that if foundIds.isEmpty() you either perform an early return (mark
all requested orderItemIds as failed) or call
assertOwnedOrderItems(order.getId(), Collections.emptyList(), storeId) / verify
order ownership first—specifically adjust the block that currently checks "if
(!foundIds.isEmpty()) { assertOwnedOrderItems(...)}" to ensure ownership is
validated even when foundIds is empty (use order.getId(), foundIds/orderItemIds,
and storeId to locate the correct checks).


List<Long> notFoundIds = uniqueOrderItemIds.stream()
.filter(id -> !foundIds.contains(id))
.toList();

List<Long> successOrderItemIds = new ArrayList<>();
List<Long> failedOrderItemIds = new ArrayList<>(notFoundIds);
List<ReturnRequest> returnRequestsToSave = new ArrayList<>();
List<OrderItemHistory> historiesToSave = new ArrayList<>();

for (OrderItem orderItem : orderItems) {
if (orderItem.requestReturn()) {
ReturnRequest returnRequest = ReturnRequest.builder()
.orderItem(orderItem)
.detailReason(command.reason())
.sellerComment(command.sellerComment())
.status(ReturnRequestRequestStatus.REQUESTED)
.build();
returnRequestsToSave.add(returnRequest);
historiesToSave.add(OrderItemHistory.create(orderItem));
successOrderItemIds.add(orderItem.getId());
} else {
failedOrderItemIds.add(orderItem.getId());
}
}

returnRequestRepository.saveAll(returnRequestsToSave);
orderItemHistoryRepository.saveAll(historiesToSave);

int successCount = successOrderItemIds.size();
int failCount = failedOrderItemIds.size();

SellerOrderResponse.Summary summary =
SellerOrderResponse.Summary.of(requestedCount, successCount, failCount);

ReturnContent content = ReturnContent.of(
order.getId(),
summary,
successOrderItemIds,
failedOrderItemIds
);

return ReturnCreateResponse.of(content);
}

private Long getStoreIdOrThrow(Long sellerId) {
Long storeId = sellerRepository.findStoreIdBySellerId(sellerId);
if (storeId == null) {
throw new BbangleException(BbangleErrorCode.SELLER_NOT_FOUND);
}
return storeId;
}

private void assertOwnedOrderItems(Long orderId, List<Long> orderItemIds, Long storeId) {
long ownedCount = orderItemRepository.countOwnedOrderItems(orderId, orderItemIds, storeId);
if (ownedCount != orderItemIds.size()) {
throw new BbangleException(BbangleErrorCode.ORDER_ACCESS_DENIED);
}
}

@Transactional
public void decision(List<Long> returnIds, Long sellerId, DecisionType decisionType, String reason) {
Expand All @@ -28,28 +132,27 @@ public void decision(List<Long> returnIds, Long sellerId, DecisionType decisionT
}

List<ReturnRequest> returnRequests = returnRequestRepository.findAllById(returnIds);
List<OrderItemHistory> historiesToSave = new ArrayList<>();

for (ReturnRequest returnRequest : returnRequests) {
processDecision(returnRequest, decisionType, reason);
OrderItemHistory history = processDecision(returnRequest, decisionType, reason);
historiesToSave.add(history);
}
orderItemHistoryRepository.saveAll(historiesToSave);
}

private void processDecision(ReturnRequest returnRequest, DecisionType decisionType, String reason) {
private OrderItemHistory processDecision(ReturnRequest returnRequest, DecisionType decisionType, String reason) {
OrderItem orderItem = returnRequest.getOrderItem();
switch (decisionType) {
case APPROVE -> {
returnRequest.approve(reason);
OrderItem orderItem = returnRequest.getOrderItem();
orderItem.returnApprove();
OrderItemHistory history = OrderItemHistory.create(orderItem);
orderItemHistoryRepository.save(history);
}
case REJECT -> {
returnRequest.reject(reason);
OrderItem orderItem = returnRequest.getOrderItem();
orderItem.returnReject();
OrderItemHistory history = OrderItemHistory.create(orderItem);
orderItemHistoryRepository.save(history);
}
}
return OrderItemHistory.create(orderItem);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.bbangle.bbangle.claim.seller.service.model;

import java.util.List;
import lombok.Builder;

@Builder
public record ExchangeCreateCommand(
Long orderId,
List<Long> orderItemIds,
String reason,
String sellerComment,
Long sellerId
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.bbangle.bbangle.claim.seller.service.model;

import java.util.List;
import lombok.Builder;

@Builder
public record ReturnCreateCommand(
Long orderId,
List<Long> orderItemIds,
String reason,
String sellerComment,
Long sellerId
) {
}
Loading