diff --git a/src/services/adminPointService.ts b/src/services/adminPointService.ts index ea5084f..fb4b06d 100644 --- a/src/services/adminPointService.ts +++ b/src/services/adminPointService.ts @@ -2,4 +2,4 @@ import { firestore } from "../firebase"; import { HttpError } from "../middleware/errors"; import { PointLedgerType } from "../types/points"; import { applyPointChangesTx } from "./pointService"; -export async function adminPointChange(uid: string, amount: number, kind: "credit" | "debit", clientIdempotencyKey: string, reason: string, actor: string, reversalOf?: string) { if (!/^[A-Za-z0-9_-]{1,100}$/.test(clientIdempotencyKey) || !Number.isSafeInteger(amount) || amount <= 0 || typeof reason !== "string" || reason.length < 1 || reason.length > 500) throw new HttpError(400, "invalid admin point change", "INVALID_INPUT"); return firestore.runTransaction((tx) => applyPointChangesTx(tx, uid, [{ txId: `${uid}:admin:${clientIdempotencyKey}`, type: kind === "credit" ? PointLedgerType.AdminCredit : PointLedgerType.AdminDebit, amount, adminReason: reason, adminActor: actor, reversalOf }])); } +export async function adminPointChange(uid: string, amount: number, kind: "credit" | "debit", clientIdempotencyKey: string, reason: string, actor: string, reversalOf?: string) { if (!/^[A-Za-z0-9_-]{1,100}$/.test(clientIdempotencyKey) || !Number.isSafeInteger(amount) || amount <= 0 || typeof reason !== "string" || reason.length < 1 || reason.length > 500) throw new HttpError(400, "invalid admin point change", "INVALID_INPUT"); return firestore.runTransaction((tx) => applyPointChangesTx(tx, uid, [{ txId: `${uid}:admin:${clientIdempotencyKey}`, type: kind === "credit" ? PointLedgerType.AdminCredit : PointLedgerType.AdminDebit, amount, adminReason: reason, adminActor: actor, ...(reversalOf !== undefined ? { reversalOf } : {}) }])); } diff --git a/tests/services/adminPointService.test.ts b/tests/services/adminPointService.test.ts new file mode 100644 index 0000000..e5b5e6d --- /dev/null +++ b/tests/services/adminPointService.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; +import { firestore } from "../../src/firebase"; +import { adminPointChange } from "../../src/services/adminPointService"; + +const uid = "admin-point-service-test-user"; + +describe("adminPointChange", () => { + it("reversalOf 없이 지급해도 원장 문서에 undefined 키가 들어가지 않는다", async () => { + // 어드민 웹의 일반 지급은 reversalOf 를 보내지 않는다 — undefined 가 Firestore 까지 + // 흘러가면 "Cannot use undefined as a Firestore value" 로 지급 전체가 실패한다. + const key = `no-reversal-${Date.now()}`; + const wallet = await adminPointChange(uid, 100, "credit", key, "테스트 지급", "admin-uid"); + + expect(wallet?.availableBalance).toBeGreaterThanOrEqual(100); + const entry = await firestore.doc(`users/${uid}/pointLedger/${uid}:admin:${key}`).get(); + expect(entry.exists).toBe(true); + expect(entry.data()).not.toHaveProperty("reversalOf"); + expect(entry.data()).toMatchObject({ adminReason: "테스트 지급", adminActor: "admin-uid", amount: 100 }); + }); + + it("reversalOf 를 명시하면 원장에 그대로 기록된다", async () => { + const key = `with-reversal-${Date.now()}`; + await adminPointChange(uid, 50, "credit", key, "정정 지급", "admin-uid", "prev-tx-id"); + + const entry = await firestore.doc(`users/${uid}/pointLedger/${uid}:admin:${key}`).get(); + expect(entry.data()).toMatchObject({ reversalOf: "prev-tx-id" }); + }); +});