- 상품에 재고 없는 옵션(optionLabel/options — 예: 키링 색상)을 도입하고 주문 항목에 optionId/optionName 스냅샷 저장 - 주문 생성 시 옵션 검증: 옵션 상품에 미선택이면 OPTION_REQUIRED, 무옵션 상품에 optionId가 오면 INVALID_INPUT - 같은 상품의 색상별 항목을 지원하도록 상품 문서를 productId당 한 번만 읽게 정리 - 재고는 별도 플랫폼에서 관리하므로 서버 재고 개념 전면 제거(ProductDoc 재고 필드, availableStock, 주문 예약/차감, stock/adjust 엔드포인트, order/status restock) - 상세 카탈로그·주문 DTO에 옵션 노출, product/upsert가 optionLabel/options 수용 - 시드/업로드 스크립트의 재고 필드 기록 제거, vitest 계약 테스트 추가
105 lines
3.2 KiB
TypeScript
105 lines
3.2 KiB
TypeScript
import { Timestamp } from "firebase-admin/firestore";
|
|
import { beforeEach, describe, expect, it } from "vitest";
|
|
import { firestore } from "../../src/firebase";
|
|
import {
|
|
getCatalog,
|
|
getCatalogProduct,
|
|
invalidateRewardCatalog,
|
|
} from "../../src/services/rewardCatalogService";
|
|
|
|
const id = "catalog-images-test";
|
|
const mainImages = [
|
|
"https://cdn.example.com/product/cover.jpg",
|
|
"https://cdn.example.com/product/gallery-2.jpg",
|
|
];
|
|
const detailImages = [
|
|
"https://cdn.example.com/product/detail-1.jpg",
|
|
"https://cdn.example.com/product/detail-2.jpg",
|
|
];
|
|
|
|
describe("rewardCatalogService image contract", () => {
|
|
beforeEach(async () => {
|
|
invalidateRewardCatalog(id);
|
|
await firestore.doc(`products/${id}`).delete();
|
|
const now = Timestamp.now();
|
|
await firestore.doc(`products/${id}`).set({
|
|
name: "이미지 계약 상품",
|
|
pointPrice: 1000,
|
|
active: true,
|
|
redeemable: true,
|
|
displayOrder: 999,
|
|
mainImages,
|
|
detailImages,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
});
|
|
});
|
|
|
|
it("목록에는 순서가 보존된 mainImages만 노출한다", async () => {
|
|
const product = (await getCatalog()).find((item) => item.id === id);
|
|
expect(product).toMatchObject({ id, mainImages });
|
|
expect(product).not.toHaveProperty("detailImages");
|
|
expect(product).not.toHaveProperty("totalStock");
|
|
expect(product).not.toHaveProperty("reservedStock");
|
|
expect(product).not.toHaveProperty("safetyStock");
|
|
expect(product).not.toHaveProperty("options");
|
|
});
|
|
|
|
it("상세에는 mainImages와 detailImages를 모두 순서대로 노출한다", async () => {
|
|
expect(await getCatalogProduct(id)).toMatchObject({
|
|
id,
|
|
mainImages,
|
|
detailImages,
|
|
});
|
|
});
|
|
|
|
it("옵션이 없는 상품은 상세에 optionLabel/options 키가 없다", async () => {
|
|
const product = await getCatalogProduct(id);
|
|
expect(product).not.toHaveProperty("optionLabel");
|
|
expect(product).not.toHaveProperty("options");
|
|
});
|
|
});
|
|
|
|
describe("rewardCatalogService option contract", () => {
|
|
const optionId = "catalog-options-test";
|
|
|
|
beforeEach(async () => {
|
|
invalidateRewardCatalog(optionId);
|
|
await firestore.doc(`products/${optionId}`).delete();
|
|
const now = Timestamp.now();
|
|
await firestore.doc(`products/${optionId}`).set({
|
|
name: "옵션 계약 상품",
|
|
pointPrice: 2600,
|
|
active: true,
|
|
redeemable: true,
|
|
displayOrder: 998,
|
|
mainImages,
|
|
detailImages,
|
|
optionLabel: "색상",
|
|
options: [
|
|
{ id: "blue", name: "블루" },
|
|
{ id: "red", name: "레드" },
|
|
],
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
});
|
|
});
|
|
|
|
it("상세에는 옵션 라벨과 선택지를 문서 순서대로 노출한다", async () => {
|
|
expect(await getCatalogProduct(optionId)).toMatchObject({
|
|
id: optionId,
|
|
optionLabel: "색상",
|
|
options: [
|
|
{ id: "blue", name: "블루" },
|
|
{ id: "red", name: "레드" },
|
|
],
|
|
});
|
|
});
|
|
|
|
it("목록에는 옵션을 노출하지 않는다", async () => {
|
|
const product = (await getCatalog()).find((item) => item.id === optionId);
|
|
expect(product).not.toHaveProperty("optionLabel");
|
|
expect(product).not.toHaveProperty("options");
|
|
});
|
|
});
|