- 주문 생성 시 예약(홀드) 없이 즉시 차감하고 confirmed 로 시작 — 상태 흐름을 확정 → 배송중 → 배송완료(+어드민 환불)로 간소화 - 유저 취소 엔드포인트(POST /orders/:id/cancel)와 cancelOrder 제거 — 상태 전이는 어드민 전용, 되돌림은 환불 전이 하나로 통일 (차감 txId 를 reversalOf 로 링크) - order_reserve/order_release 원장 타입과 reserve/capture/release op, 지갑 reservedBalance 제거 — 불변식을 available = earned - spent 로 단순화 - 이전 데이터 전면 삭제에 따라 2026-07 개편 이전 원장 문서용 레거시 정규화(StoredLedgerEntry, balanceAfter 역산, refMonth/refDay 합성)도 제거 - 주문/원장/지갑 테스트를 새 스키마로 갱신하고 orderService 에뮬레이터 테스트 6건 추가 (즉시 차감·잔액 부족·권한·전이 검증·환불 반환)
73 lines
3.2 KiB
TypeScript
73 lines
3.2 KiB
TypeScript
import { beforeEach, describe, expect, it } from "vitest";
|
|
import { Timestamp } from "firebase-admin/firestore";
|
|
import { firestore } from "../../src/firebase";
|
|
import { listAllOrders } from "../../src/repositories/orderRepository";
|
|
import type { OrderDoc } from "../../src/types/reward";
|
|
|
|
function makeOrder(uid: string, createdAt: Timestamp, overrides: Partial<OrderDoc> = {}): OrderDoc {
|
|
return {
|
|
uid,
|
|
items: [{ productId: "p1", qty: 1, pointPrice: 500, name: "굿즈" }],
|
|
totalPoints: 500,
|
|
recipient: { name: "홍길동", phone: "01000000000", address1: "서울", postalCode: "00000" },
|
|
status: "confirmed",
|
|
clientIdempotencyKey: `key-${uid}-${createdAt.toMillis()}`,
|
|
debitLedgerTxId: `${uid}:order:test:debit`,
|
|
orderedAt: createdAt,
|
|
confirmedAt: createdAt,
|
|
statusHistory: [{ status: "confirmed", at: createdAt, actor: uid }],
|
|
createdAt,
|
|
updatedAt: createdAt,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("orderRepository.listAllOrders", () => {
|
|
beforeEach(async () => {
|
|
await firestore.recursiveDelete(firestore.collection("orders"));
|
|
});
|
|
|
|
it("uid 필터 없이 서로 다른 유저의 주문을 모두 createdAt desc로 반환한다", async () => {
|
|
const t1 = Timestamp.fromDate(new Date("2026-07-01T00:00:00.000Z"));
|
|
const t2 = Timestamp.fromDate(new Date("2026-07-02T00:00:00.000Z"));
|
|
const t3 = Timestamp.fromDate(new Date("2026-07-03T00:00:00.000Z"));
|
|
|
|
await firestore.doc("orders/o1").set(makeOrder("uid-a", t1));
|
|
await firestore.doc("orders/o2").set(makeOrder("uid-b", t2));
|
|
await firestore.doc("orders/o3").set(makeOrder("uid-a", t3));
|
|
|
|
const page = await listAllOrders(20);
|
|
expect(page.items.map((o) => o.id)).toEqual(["o3", "o2", "o1"]);
|
|
expect(page.items.map((o) => o.uid).sort()).toEqual(["uid-a", "uid-a", "uid-b"].sort());
|
|
});
|
|
|
|
it("cursor 기반 페이지네이션이 동작한다", async () => {
|
|
const t1 = Timestamp.fromDate(new Date("2026-07-01T00:00:00.000Z"));
|
|
const t2 = Timestamp.fromDate(new Date("2026-07-02T00:00:00.000Z"));
|
|
const t3 = Timestamp.fromDate(new Date("2026-07-03T00:00:00.000Z"));
|
|
|
|
await firestore.doc("orders/o1").set(makeOrder("uid-a", t1));
|
|
await firestore.doc("orders/o2").set(makeOrder("uid-b", t2));
|
|
await firestore.doc("orders/o3").set(makeOrder("uid-a", t3));
|
|
|
|
const firstPage = await listAllOrders(2);
|
|
expect(firstPage.items.map((o) => o.id)).toEqual(["o3", "o2"]);
|
|
expect(firstPage.cursor).toBe("o2");
|
|
|
|
const secondPage = await listAllOrders(2, firstPage.cursor ?? undefined);
|
|
expect(secondPage.items.map((o) => o.id)).toEqual(["o1"]);
|
|
expect(secondPage.cursor).toBe("o1");
|
|
});
|
|
|
|
it("status 필터를 걸면 해당 상태 주문만 반환한다 (기존 status+createdAt 인덱스 재사용)", async () => {
|
|
const t1 = Timestamp.fromDate(new Date("2026-07-01T00:00:00.000Z"));
|
|
const t2 = Timestamp.fromDate(new Date("2026-07-02T00:00:00.000Z"));
|
|
|
|
await firestore.doc("orders/o1").set(makeOrder("uid-a", t1, { status: "shipped" }));
|
|
await firestore.doc("orders/o2").set(makeOrder("uid-b", t2, { status: "confirmed" }));
|
|
|
|
const page = await listAllOrders(20, undefined, "confirmed");
|
|
expect(page.items.map((o) => o.id)).toEqual(["o2"]);
|
|
});
|
|
});
|