mmday-firebase/tests/services/rewardCatalogService.test.ts
윤정민 6343a817af Add reward goods catalog and point-based order flow
- 상품 카탈로그(products/) 신설: 공개 필드(name·pointPrice·이미지)와 재고(total/reserved/safety)를 한 문서로 관리, 목록·상세는 60초 인메모리 캐시로 서빙
- 주문 생성 시 포인트 예약(order_reserve)과 재고 예약을 단일 트랜잭션으로 원자 처리, 주문 ID를 uid_클라이언트멱등키로 고정해 더블탭·재시도 중복 생성 차단
- 주문 상태 머신(reserved→confirmed→preparing→shipped→delivered / cancelled / refunded) 전이 검증과 전이별 capture·release·refund 역거래 원장 기록
- reward HTTP 함수 신설: 지갑·원장 조회, 상품 목록·상세, 교환 자격, 주문 생성·목록·상세·취소 API
- 관리자 라우트 추가: 상품 등록·수정, 재고 조정(가용재고 음수 방지), 주문 상태 변경, 포인트 지급·회수
- Firestore 보안규칙(지갑 본인 read, 상품·주문은 API 전용)과 orders 복합 인덱스(uid+createdAt, status+createdAt) 추가
- 상품 4종 시드(seed:rewards)와 상세 이미지 업로드(upload:reward-assets) 스크립트 추가
2026-07-16 14:25:58 +09:00

58 lines
1.7 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,
totalStock: 10,
reservedStock: 2,
safetyStock: 1,
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");
});
it("상세에는 mainImages와 detailImages를 모두 순서대로 노출한다", async () => {
expect(await getCatalogProduct(id)).toMatchObject({
id,
mainImages,
detailImages,
});
});
});