import { describe, expect, it } from "vitest"; import { LoyaltyCardFont, LoyaltyCardTextSize, RewardPositionMode } from "@/generated/prisma/enums"; import { resolveRewardThreshold, validateProgrammeConfig } from "../loyalty.service"; import type { ProgrammeConfigInput } from "../loyalty.service"; const base: ProgrammeConfigInput = { isEnabled: true, stampsRequired: 10, rewardPosition: RewardPositionMode.AFTER_ALL_STAMPS, rewardType: "PERCENTAGE", rewardValue: "50", autoApplyReward: true, requiresStaffApproval: false, earnOnCancelled: false, earnOnNoShow: false, cardFont: LoyaltyCardFont.SANS, cardTitleTextSize: LoyaltyCardTextSize.MD, }; describe("validateProgrammeConfig", () => { it("accepts a valid percentage-reward configuration", () => { expect(validateProgrammeConfig(base).valid).toBe(true); }); it("rejects a stamps-required of zero", () => { const result = validateProgrammeConfig({ ...base, stampsRequired: 0 }); expect(result.valid).toBe(false); expect(result.issues[0]?.code).toBe("INVALID_INPUT"); }); it("allows an incomplete configuration while disabled", () => { const result = validateProgrammeConfig({ ...base, isEnabled: false, rewardValue: undefined }); expect(result.valid).toBe(true); }); it.each([0, 101, -5])("rejects an out-of-range percentage when enabled (%s)", (pct) => { const result = validateProgrammeConfig({ ...base, rewardValue: String(pct) }); expect(result.valid).toBe(false); expect(result.issues[0]?.code).toBe("INVALID_PERCENTAGE"); }); it("rejects a missing percentage value when enabled", () => { const result = validateProgrammeConfig({ ...base, rewardValue: undefined }); expect(result.valid).toBe(false); expect(result.issues[0]?.code).toBe("INVALID_PERCENTAGE"); }); it("accepts a valid fixed-amount reward", () => { const result = validateProgrammeConfig({ ...base, rewardType: "FIXED_AMOUNT", rewardValue: "10.00" }); expect(result.valid).toBe(true); }); it("rejects a negative fixed-amount reward", () => { const result = validateProgrammeConfig({ ...base, rewardType: "FIXED_AMOUNT", rewardValue: "-1" }); expect(result.valid).toBe(false); expect(result.issues[0]?.code).toBe("INVALID_VALUE"); }); it("accepts a free-service reward with a service chosen", () => { const result = validateProgrammeConfig({ ...base, rewardType: "FREE_SERVICE", freeServiceId: "svc-1" }); expect(result.valid).toBe(true); }); it("rejects a free-service reward with no service chosen", () => { const result = validateProgrammeConfig({ ...base, rewardType: "FREE_SERVICE", freeServiceId: undefined }); expect(result.valid).toBe(false); expect(result.issues[0]?.code).toBe("FREE_SERVICE_REQUIRED"); }); }); describe("resolveRewardThreshold", () => { it("AFTER_ALL_STAMPS requires every position filled", () => { expect(resolveRewardThreshold({ stampsRequired: 10, rewardPosition: RewardPositionMode.AFTER_ALL_STAMPS })).toBe(10); }); it("FINAL_SLOT_IS_REWARD is earned one stamp early — the last position is the reward, not a stamp to collect", () => { expect( resolveRewardThreshold({ stampsRequired: 10, rewardPosition: RewardPositionMode.FINAL_SLOT_IS_REWARD }), ).toBe(9); }); it("clamps FINAL_SLOT_IS_REWARD at a minimum threshold of 1, even for a single-stamp card", () => { expect( resolveRewardThreshold({ stampsRequired: 1, rewardPosition: RewardPositionMode.FINAL_SLOT_IS_REWARD }), ).toBe(1); }); });