feat : limit commis

This commit is contained in:
2026-10-01 21:32:59 +07:00 Unverified
parent 2517982a27
commit 9fc12c1874
13 changed files with 4129 additions and 6 deletions
+43
View File
@@ -0,0 +1,43 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { HttpError } from "@/lib/security/http";
const requireAdmin = vi.fn();
const update = vi.fn();
const set = vi.fn();
const where = vi.fn();
const returning = vi.fn();
vi.mock("@/lib/auth/server", () => ({ requireAdmin }));
vi.mock("@/db", () => ({ getDb: () => ({ update }) }));
const { setCommissionAvailability } = await import("./actions");
describe("admin commission availability", () => {
beforeEach(() => {
vi.clearAllMocks();
requireAdmin.mockResolvedValue({ user: { id: "admin-1" } });
update.mockReturnValue({ set });
set.mockReturnValue({ where });
where.mockReturnValue({ returning });
returning.mockResolvedValue([{ isOpen: false }]);
});
it("requires admin authorization", async () => {
requireAdmin.mockRejectedValueOnce(new HttpError(401, "unauthorized"));
await expect(setCommissionAvailability(false, "Closed today")).rejects.toMatchObject({ status: 401 });
expect(update).not.toHaveBeenCalled();
});
it("requires a closure message and saves the trimmed text", async () => {
expect(await setCommissionAvailability(false, " ")).toMatchObject({ status: "error" });
expect(update).not.toHaveBeenCalled();
expect(await setCommissionAvailability(false, " Closed today ")).toEqual({ status: "saved" });
expect(set).toHaveBeenCalledWith({ isOpen: false, closureMessage: "Closed today",
updatedAt: expect.any(Date) });
});
it("reopens without clearing the saved explanation", async () => {
expect(await setCommissionAvailability(true, "Previous reason")).toEqual({ status: "saved" });
expect(set).toHaveBeenCalledWith({ isOpen: true, updatedAt: expect.any(Date) });
});
});
+24
View File
@@ -0,0 +1,24 @@
"use server";
import { eq } from "drizzle-orm";
import { getDb } from "@/db";
import { commissionSettings } from "@/db/schema";
import { requireAdmin } from "@/lib/auth/server";
export async function setCommissionAvailability(isOpen: boolean, message: string) {
await requireAdmin();
if (typeof isOpen !== "boolean" || typeof message !== "string")
return { status: "error" as const, message: "ข้อมูลไม่ถูกต้อง" };
const closureMessage = message.trim();
if (!isOpen && (closureMessage.length < 1 || closureMessage.length > 1000))
return { status: "error" as const, message: "กรุณาเขียนเหตุผลที่ปิดรับ Commission (ไม่เกิน 1,000 ตัวอักษร)" };
try {
const [settings] = await getDb().update(commissionSettings).set({
isOpen, ...(!isOpen ? { closureMessage } : {}), updatedAt: new Date(),
}).where(eq(commissionSettings.id, 1)).returning({ isOpen: commissionSettings.isOpen });
if (!settings) return { status: "error" as const, message: "ไม่พบการตั้งค่า Commission" };
return { status: "saved" as const };
} catch {
return { status: "error" as const, message: "บันทึกสถานะ Commission ไม่สำเร็จ" };
}
}
+6 -2
View File
@@ -6,9 +6,11 @@ import { getDb } from "@/db";
import { commissionCheckouts, commissionTickets, users } from "@/db/schema";
import { AdminHeader } from "@/components/admin/admin-header";
import { CommissionLiveRefresh } from "@/components/commission/live-refresh";
import { AdminCommissionAvailabilityControl } from "@/components/commission/admin-availability-control";
import { Badge } from "@/components/ui/badge";
import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { getAdminSession } from "@/lib/auth/server";
import { getCommissionAvailability } from "@/lib/commission/availability";
export const instant = false;
export default async function AdminCommissionPage({ searchParams }: PageProps<"/admin/commission">) {
@@ -17,15 +19,17 @@ export default async function AdminCommissionPage({ searchParams }: PageProps<"/
const query = await searchParams;
const rawPage = Number(query.page ?? 1);
const page = Number.isInteger(rawPage) && rawPage > 0 ? Math.min(rawPage, 10000) : 1;
const tickets = await getDb().select({ ticket: commissionTickets, amount: commissionCheckouts.amountBaht,
const [tickets, availability] = await Promise.all([getDb().select({ ticket: commissionTickets, amount: commissionCheckouts.amountBaht,
customer: users.name }).from(commissionTickets)
.innerJoin(commissionCheckouts, eq(commissionTickets.checkoutId, commissionCheckouts.id))
.innerJoin(users, eq(commissionTickets.userId, users.id))
.orderBy(desc(commissionTickets.status), desc(commissionTickets.updatedAt), desc(commissionTickets.id))
.limit(51).offset((page - 1) * 50);
.limit(51).offset((page - 1) * 50), getCommissionAvailability()]);
return <div className="min-h-svh"><AdminHeader /><CommissionLiveRefresh endpoint="/api/commission/events?scope=admin" />
<main className="mx-auto flex max-w-5xl flex-col gap-5 p-4 py-10 sm:p-8">
<h1 className="font-heading text-3xl font-semibold">Commission tickets</h1>
<AdminCommissionAvailabilityControl key={`${availability.isOpen}:${availability.closureMessage}`}
{...availability} />
{tickets.length === 0 && <p>ยังไม่มี Ticket</p>}
{tickets.slice(0, 50).map(({ ticket, amount, customer }) => <Link key={ticket.id} href={`/admin/commission/${ticket.id}`}>
<Card><CardHeader><CardTitle>{ticket.title || `#${ticket.id.slice(0, 8)}`} - {customer}</CardTitle>