- 상품 카탈로그(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) 스크립트 추가
51 lines
1.5 KiB
TypeScript
51 lines
1.5 KiB
TypeScript
import { beforeEach, describe, expect, it } from "vitest";
|
|
import { firestore } from "../../src/firebase";
|
|
import { computeEligibility } from "../../src/services/eligibilityService";
|
|
import { applyPointChanges } from "../../src/services/pointService";
|
|
import { PointLedgerType } from "../../src/types/points";
|
|
|
|
const uid = "eligibility-points-only";
|
|
|
|
describe("computeEligibility", () => {
|
|
beforeEach(async () => {
|
|
await firestore.recursiveDelete(firestore.doc(`users/${uid}`));
|
|
});
|
|
|
|
it("상점 진입 자격은 가용 포인트 보유 여부만 확인한다", async () => {
|
|
expect(await computeEligibility(uid)).toEqual({
|
|
eligible: false,
|
|
reasons: ["INSUFFICIENT_BALANCE"],
|
|
availableBalance: 0,
|
|
});
|
|
|
|
await applyPointChanges(uid, [{
|
|
txId: `${uid}:admin:seed`,
|
|
type: PointLedgerType.AdminCredit,
|
|
amount: 100,
|
|
}]);
|
|
|
|
expect(await computeEligibility(uid)).toEqual({
|
|
eligible: true,
|
|
reasons: [],
|
|
availableBalance: 100,
|
|
});
|
|
});
|
|
|
|
it("주문 자격은 주문 총액 이상의 가용 포인트만 확인한다", async () => {
|
|
await applyPointChanges(uid, [{
|
|
txId: `${uid}:admin:seed-order`,
|
|
type: PointLedgerType.AdminCredit,
|
|
amount: 100,
|
|
}]);
|
|
|
|
expect(await computeEligibility(uid, 101)).toMatchObject({
|
|
eligible: false,
|
|
reasons: ["INSUFFICIENT_BALANCE"],
|
|
});
|
|
expect(await computeEligibility(uid, 100)).toMatchObject({
|
|
eligible: true,
|
|
reasons: [],
|
|
});
|
|
});
|
|
});
|