Skip to content
Merged
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
23 changes: 23 additions & 0 deletions src/features/order/repositories/order.repository.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,29 @@ describe('OrderRepository (real DB)', () => {
});
});

describe('findReviewableOrderIds', () => {
it('soft-delete된 주문의 아이템은 리뷰 가능 집계에서 제외한다', async () => {
const buyer = await setupBuyer();
const active = await createOrder(prisma, {
account_id: buyer.id,
status: 'PICKED_UP',
});
await createOrderItem(prisma, { order_id: active.id });
const deleted = await createOrder(prisma, {
account_id: buyer.id,
status: 'PICKED_UP',
deleted_at: new Date(),
});
await createOrderItem(prisma, { order_id: deleted.id });

const ids = await repo.findReviewableOrderIds({
accountId: buyer.id,
orderIds: [active.id, deleted.id],
});
expect(ids).toEqual(new Set([active.id.toString()]));
});
});

describe('findOrderDetailByAccount', () => {
it('본인 주문이면 상세 반환 (status_histories 포함)', async () => {
const buyer = await setupBuyer();
Expand Down
13 changes: 12 additions & 1 deletion src/features/order/repositories/order.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,9 @@ export class OrderRepository {
order: {
account_id: args.accountId,
status: OrderStatus.PICKED_UP,
// 삭제된 주문의 아이템이 리뷰 가능으로 집계되지 않게 명시
// (listReviewableOrderItems와 동일 가드)
...activeWhere,
},
OR: [
{ review: { is: null } },
Expand Down Expand Up @@ -557,6 +560,7 @@ export class OrderRepository {
items: {
some: {
store_id: args.storeId,
...activeWhere,
},
},
},
Expand All @@ -572,28 +576,35 @@ export class OrderRepository {
items: {
some: {
store_id: args.storeId,
...activeWhere,
},
},
},
// 유저측 상세(findOrderDetailByUser)와 동일하게 soft-delete 자식을 가드한다
include: {
status_histories: {
where: activeWhere,
orderBy: {
changed_at: 'desc',
},
},
items: {
where: {
store_id: args.storeId,
...activeWhere,
},
include: {
option_items: true,
option_items: { where: activeWhere },
custom_texts: {
where: activeWhere,
orderBy: { sort_order: 'asc' },
},
free_edits: {
where: activeWhere,
orderBy: { sort_order: 'asc' },
include: {
attachments: {
where: activeWhere,
orderBy: { sort_order: 'asc' },
},
},
Expand Down
122 changes: 122 additions & 0 deletions src/features/product/repositories/product.repository.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,44 @@ describe('ProductRepository (real DB)', () => {

// ─── Product list/fetch ──
describe('listProductsByStore', () => {
it('삭제된 카테고리 연결·태그로는 목록 필터에 걸리지 않는다', async () => {
const store = await createStore(prisma);
const product = await createProduct(prisma, {
store_id: store.id,
name: '무관한 이름',
});
const category = await createCategory('생일');
await prisma.productCategory.create({
data: {
product_id: product.id,
category_id: category.id,
deleted_at: new Date(),
},
});
const tag = await createTag('레터링');
await prisma.productTag.create({
data: {
product_id: product.id,
tag_id: tag.id,
deleted_at: new Date(),
},
});

const byCategory = await repo.listProductsByStore({
storeId: store.id,
limit: 10,
categoryId: category.id,
});
expect(byCategory).toHaveLength(0);

const bySearch = await repo.listProductsByStore({
storeId: store.id,
limit: 10,
search: '레터링',
});
expect(bySearch).toHaveLength(0);
});

it('store_id 필터 + cursor 페이지네이션', async () => {
const storeA = await createStore(prisma);
const storeB = await createStore(prisma);
Expand Down Expand Up @@ -209,6 +247,50 @@ describe('ProductRepository (real DB)', () => {
});
expect(result).toBeNull();
});

it('soft-delete된 카테고리·태그 연결과 삭제된 대상은 제외한다', async () => {
const store = await createStore(prisma);
const product = await createProduct(prisma, { store_id: store.id });
const liveCategory = await createCategory('생일');
const linkDeletedCategory = await createCategory('링크 삭제');
const deletedCategory = await createCategory('대상 삭제');
await prisma.productCategory.createMany({
data: [
{ product_id: product.id, category_id: liveCategory.id },
{
product_id: product.id,
category_id: linkDeletedCategory.id,
deleted_at: new Date(),
},
{ product_id: product.id, category_id: deletedCategory.id },
],
});
await prisma.category.update({
where: { id: deletedCategory.id },
data: { deleted_at: new Date() },
});
const liveTag = await createTag('레터링');
const linkDeletedTag = await createTag('링크 삭제 태그');
await prisma.productTag.createMany({
data: [
{ product_id: product.id, tag_id: liveTag.id },
{
product_id: product.id,
tag_id: linkDeletedTag.id,
deleted_at: new Date(),
},
],
});

const result = await repo.findProductById({
productId: product.id,
storeId: store.id,
});
expect(result?.product_categories.map((c) => c.category.name)).toEqual([
'생일',
]);
expect(result?.product_tags.map((t) => t.tag.name)).toEqual(['레터링']);
});
});

describe('findProductByIdIncludingInactive', () => {
Expand Down Expand Up @@ -441,6 +523,26 @@ describe('ProductRepository (real DB)', () => {
expect(result.option_items).toHaveLength(1);
});

it('findOptionGroupById·listOptionGroupsByProduct는 soft-delete된 옵션 아이템을 제외한다', async () => {
const store = await createStore(prisma);
const product = await createProduct(prisma, { store_id: store.id });
const group = await createOptionGroup(product.id);
const live = await createOptionItem(group.id);
await prisma.productOptionItem.create({
data: {
option_group_id: group.id,
title: '삭제된 항목',
deleted_at: new Date(),
},
});

const found = await repo.findOptionGroupById(group.id);
expect(found?.option_items.map((i) => i.id)).toEqual([live.id]);

const rows = await repo.listOptionGroupsByProduct(product.id);
expect(rows[0].option_items.map((i) => i.id)).toEqual([live.id]);
});

it('softDeleteOptionGroup: deleted_at + is_active:false', async () => {
const store = await createStore(prisma);
const product = await createProduct(prisma, { store_id: store.id });
Expand Down Expand Up @@ -557,6 +659,26 @@ describe('ProductRepository (real DB)', () => {
expect(second.is_active).toBe(false);
});

it('findCustomTemplateById는 soft-delete된 텍스트 토큰을 제외한다', async () => {
const store = await createStore(prisma);
const product = await createProduct(prisma, { store_id: store.id });
const tpl = await createTemplate(product.id);
const live = await prisma.productCustomTextToken.create({
data: { template_id: tpl.id, token_key: 'live', default_text: '문구' },
});
await prisma.productCustomTextToken.create({
data: {
template_id: tpl.id,
token_key: 'deleted',
default_text: '삭제',
deleted_at: new Date(),
},
});

const found = await repo.findCustomTemplateById(tpl.id);
expect(found?.text_tokens.map((t) => t.id)).toEqual([live.id]);
});

it('findCustomTemplateById + setCustomTemplateActive', async () => {
const store = await createStore(prisma);
const product = await createProduct(prisma, { store_id: store.id });
Expand Down
20 changes: 20 additions & 0 deletions src/features/product/repositories/product.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,12 @@ export class ProductRepository {
...(args.isActive !== undefined ? { is_active: args.isActive } : {}),
...(args.categoryId
? {
// include의 링크·대상 가드와 동일 — 삭제된 연결이 필터에 걸리지 않게 한다
product_categories: {
some: {
category_id: args.categoryId,
...activeWhere,
category: activeWhere,
},
},
}
Expand All @@ -126,8 +129,10 @@ export class ProductRepository {
{
product_tags: {
some: {
...activeWhere,
tag: {
name: { contains: args.search },
...activeWhere,
},
},
},
Expand All @@ -143,11 +148,15 @@ export class ProductRepository {
orderBy: { sort_order: 'asc' },
},
product_categories: {
// 링크·대상 카테고리의 soft-delete 가드. is_active는 셀러 화면에서
// 기존 지정을 계속 보여줘야 하므로 걸지 않는다.
where: { ...activeWhere, category: activeWhere },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Apply soft-delete guards to seller-list predicates

When sellerProducts filters by a category or searches by a tag that is linked only through a soft-deleted ProductCategory/ProductTag (or a deleted target), the top-level some predicates still match because they lack activeWhere. These new include guards then hide the matching relation while the product remains in the results, so deleted taxonomy continues to affect seller filtering and search; apply the same link and target guards to the relation predicates in listProductsByStore.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

반영: listProductsByStore의 categoryId·search 태그 predicate에도 include와 동일한 링크·대상 soft-delete 가드 추가. 회귀 테스트 1건 동반.

include: {
category: true,
},
},
product_tags: {
where: { ...activeWhere, tag: activeWhere },
include: {
tag: true,
},
Expand Down Expand Up @@ -206,11 +215,15 @@ export class ProductRepository {
orderBy: { sort_order: 'asc' },
},
product_categories: {
// 링크·대상 카테고리의 soft-delete 가드. is_active는 셀러 화면에서
// 기존 지정을 계속 보여줘야 하므로 걸지 않는다.
where: { ...activeWhere, category: activeWhere },
include: {
category: true,
},
},
product_tags: {
where: { ...activeWhere, tag: activeWhere },
include: {
tag: true,
},
Expand Down Expand Up @@ -253,11 +266,15 @@ export class ProductRepository {
orderBy: { sort_order: 'asc' },
},
product_categories: {
// 링크·대상 카테고리의 soft-delete 가드. is_active는 셀러 화면에서
// 기존 지정을 계속 보여줘야 하므로 걸지 않는다.
where: { ...activeWhere, category: activeWhere },
include: {
category: true,
},
},
product_tags: {
where: { ...activeWhere, tag: activeWhere },
include: {
tag: true,
},
Expand Down Expand Up @@ -478,6 +495,7 @@ export class ProductRepository {
},
},
option_items: {
where: activeWhere,
orderBy: { sort_order: 'asc' },
},
},
Expand Down Expand Up @@ -515,6 +533,7 @@ export class ProductRepository {
orderBy: { sort_order: 'asc' },
include: {
option_items: {
where: activeWhere,
orderBy: { sort_order: 'asc' },
},
},
Expand Down Expand Up @@ -662,6 +681,7 @@ export class ProductRepository {
},
},
text_tokens: {
where: activeWhere,
orderBy: { sort_order: 'asc' },
},
},
Expand Down
38 changes: 38 additions & 0 deletions src/features/seller/services/seller-order.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,44 @@ describe('SellerOrderService (real DB)', () => {
expect(result.statusHistories).toHaveLength(1);
expect(result.statusHistories[0].toStatus).toBe('CONFIRMED');
});

it('soft-delete된 아이템·상태 이력은 상세에서 제외한다', async () => {
const { account, store } = await setupSellerWithStore(prisma);
const order = await createStoreOrder(store.id, { status: 'CONFIRMED' });
await createOrderItem(prisma, {
order_id: order.id,
store_id: store.id,
deleted_at: new Date(),
});
await prisma.orderStatusHistory.create({
data: {
order_id: order.id,
from_status: 'SUBMITTED',
to_status: 'CONFIRMED',
changed_at: new Date('2026-04-15T10:00:00Z'),
deleted_at: new Date(),
},
});

const result = await service.sellerOrder(account.id, order.id);
expect(result.items).toHaveLength(1);
expect(result.statusHistories).toHaveLength(0);
});

it('soft-delete된 아이템만 있는 주문은 상세·목록 모두에서 제외한다', async () => {
const { account, store } = await setupSellerWithStore(prisma);
const order = await createStoreOrder(store.id);
await prisma.orderItem.updateMany({
where: { order_id: order.id },
data: { deleted_at: new Date() },
});

await expect(service.sellerOrder(account.id, order.id)).rejects.toThrow(
NotFoundException,
);
const list = await service.sellerOrderList(account.id);
expect(list.items).toHaveLength(0);
});
});

describe('sellerUpdateOrderStatus', () => {
Expand Down
Loading
Loading