feat : limit commis
This commit is contained in:
@@ -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) });
|
||||
});
|
||||
});
|
||||
@@ -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,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>
|
||||
|
||||
Reference in New Issue
Block a user