feat : add slipok and switcher to switch between slip2go and slipok
This commit is contained in:
@@ -8,6 +8,8 @@ const where = vi.fn();
|
||||
const returning = vi.fn();
|
||||
|
||||
vi.mock("@/lib/auth/server", () => ({ requireAdmin }));
|
||||
vi.mock("server-only", () => ({}));
|
||||
vi.mock("next/cache", () => ({ refresh: vi.fn() }));
|
||||
vi.mock("@/db", () => ({ getDb: () => ({ update }) }));
|
||||
|
||||
const { setCommissionAvailability } = await import("./actions");
|
||||
|
||||
@@ -4,6 +4,8 @@ import { eq } from "drizzle-orm";
|
||||
import { getDb } from "@/db";
|
||||
import { commissionSettings } from "@/db/schema";
|
||||
import { requireAdmin } from "@/lib/auth/server";
|
||||
import { refresh } from "next/cache";
|
||||
import { isSlipProvider, slipProviderConfigured } from "@/lib/commission/slip-provider";
|
||||
|
||||
export async function setCommissionAvailability(isOpen: boolean, message: string) {
|
||||
await requireAdmin();
|
||||
@@ -22,3 +24,20 @@ export async function setCommissionAvailability(isOpen: boolean, message: string
|
||||
return { status: "error" as const, message: "บันทึกสถานะ Commission ไม่สำเร็จ" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function setCommissionSlipProvider(provider: unknown) {
|
||||
await requireAdmin();
|
||||
if (!isSlipProvider(provider)) return { status: "error" as const, message: "ผู้ให้บริการไม่ถูกต้อง" };
|
||||
if (!slipProviderConfigured(provider))
|
||||
return { status: "error" as const, message: "ยังไม่ได้ตั้งค่าผู้ให้บริการนี้บนเซิร์ฟเวอร์" };
|
||||
try {
|
||||
const [settings] = await getDb().update(commissionSettings).set({
|
||||
slipProvider: provider, updatedAt: new Date(),
|
||||
}).where(eq(commissionSettings.id, 1)).returning({ slipProvider: commissionSettings.slipProvider });
|
||||
if (!settings) return { status: "error" as const, message: "ไม่พบการตั้งค่า Commission" };
|
||||
} catch {
|
||||
return { status: "error" as const, message: "บันทึกผู้ให้บริการตรวจสลิปไม่สำเร็จ" };
|
||||
}
|
||||
refresh();
|
||||
return { status: "saved" as const };
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import { Button } from "@/components/ui/button";
|
||||
import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Pencil } from "lucide-react";
|
||||
import { getAdminSession } from "@/lib/auth/server";
|
||||
import { AdminCommissionSlipProviderControl } from "@/components/commission/admin-slip-provider-control";
|
||||
import { getCommissionSlipProvider, slipProviderConfigured } from "@/lib/commission/slip-provider";
|
||||
import { getCommissionAvailability } from "@/lib/commission/availability";
|
||||
|
||||
export const instant = false;
|
||||
@@ -23,15 +25,19 @@ 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, availability] = await Promise.all([getDb().select({ ticket: commissionTickets, amount: commissionCheckouts.amountBaht }).from(commissionTickets)
|
||||
const [tickets, availability, slipProvider] = await Promise.all([getDb().select({ ticket: commissionTickets, amount: commissionCheckouts.amountBaht }).from(commissionTickets)
|
||||
.innerJoin(commissionCheckouts, eq(commissionTickets.checkoutId, commissionCheckouts.id))
|
||||
.orderBy(desc(commissionTickets.status), desc(commissionTickets.updatedAt), desc(commissionTickets.id))
|
||||
.limit(51).offset((page - 1) * 50), getCommissionAvailability()]);
|
||||
.limit(51).offset((page - 1) * 50), getCommissionAvailability(), getCommissionSlipProvider()]);
|
||||
return <div className="min-h-svh"><AdminHeader /><CommissionLiveRefresh endpoint="/api/commission/events?scope=admin" userId={session.user.id} />
|
||||
<main className="mx-auto flex max-w-5xl flex-col gap-5 p-4 py-10 sm:p-8">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<h1 className="font-heading text-3xl font-semibold">Commission tickets</h1>
|
||||
<Button variant="outline" nativeButton={false} render={<Link href="/admin/commission/income" />}>ดูรายได้ประจำเดือน</Button>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<AdminCommissionSlipProviderControl key={slipProvider} provider={slipProvider}
|
||||
configured={{ slip2go: slipProviderConfigured("slip2go"), slipok: slipProviderConfigured("slipok") }} />
|
||||
<Button variant="outline" nativeButton={false} render={<Link href="/admin/commission/income" />}>ดูรายได้ประจำเดือน</Button>
|
||||
</div>
|
||||
</div>
|
||||
<AdminCommissionAvailabilityControl key={`${availability.isOpen}:${availability.closureMessage}`}
|
||||
{...availability} />
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { requireAdmin, update, set, where, returning, refresh } = vi.hoisted(() => ({
|
||||
requireAdmin: vi.fn(), update: vi.fn(), set: vi.fn(), where: vi.fn(), returning: vi.fn(), refresh: vi.fn(),
|
||||
}));
|
||||
vi.mock("server-only", () => ({}));
|
||||
vi.mock("@/lib/auth/server", () => ({ requireAdmin }));
|
||||
vi.mock("@/db", () => ({ getDb: () => ({ update }) }));
|
||||
vi.mock("next/cache", () => ({ refresh }));
|
||||
import { setCommissionSlipProvider } from "./actions";
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
requireAdmin.mockResolvedValue({ user: { id: "admin" } });
|
||||
update.mockReturnValue({ set }); set.mockReturnValue({ where }); where.mockReturnValue({ returning });
|
||||
returning.mockResolvedValue([{ slipProvider: "slipok" }]);
|
||||
vi.stubEnv("SLIPOK_BRANCH_ID", "123"); vi.stubEnv("SLIPOK_API_KEY", "secret");
|
||||
});
|
||||
afterEach(() => vi.unstubAllEnvs());
|
||||
|
||||
describe("admin slip provider settings", () => {
|
||||
it("requires authorization before updating settings", async () => {
|
||||
requireAdmin.mockRejectedValue(new Error("unauthorized"));
|
||||
await expect(setCommissionSlipProvider("slipok")).rejects.toThrow("unauthorized");
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
});
|
||||
it.each(["invalid", null, { slipProvider: "slipok" }])("rejects invalid input", async (value) => {
|
||||
expect(await setCommissionSlipProvider(value)).toMatchObject({ status: "error" });
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
});
|
||||
it.each(["", "abc", "123/other"])("rejects unconfigured or invalid branch IDs", async (branch) => {
|
||||
vi.stubEnv("SLIPOK_BRANCH_ID", branch);
|
||||
expect(await setCommissionSlipProvider("slipok")).toMatchObject({ status: "error" });
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
});
|
||||
it("rejects a missing key", async () => {
|
||||
vi.stubEnv("SLIPOK_API_KEY", " ");
|
||||
expect(await setCommissionSlipProvider("slipok")).toMatchObject({ status: "error" });
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
});
|
||||
it("saves only the provider and timestamp and refreshes the current view", async () => {
|
||||
expect(await setCommissionSlipProvider("slipok")).toEqual({ status: "saved" });
|
||||
expect(set).toHaveBeenCalledWith({ slipProvider: "slipok", updatedAt: expect.any(Date) });
|
||||
expect(refresh).toHaveBeenCalledOnce();
|
||||
});
|
||||
it("supports switching back to Slip2Go", async () => {
|
||||
vi.stubEnv("SLIP2GO_VERIFY_URL", "https://slip2go.test/verify");
|
||||
vi.stubEnv("SLIP2GO_API_SECRET", "secret");
|
||||
expect(await setCommissionSlipProvider("slip2go")).toEqual({ status: "saved" });
|
||||
expect(set).toHaveBeenCalledWith({ slipProvider: "slip2go", updatedAt: expect.any(Date) });
|
||||
});
|
||||
it.each([{ result: [] }, { result: "throw" }])("reports missing settings or failed persistence", async ({ result }) => {
|
||||
if (result === "throw") returning.mockRejectedValueOnce(new Error("database error"));
|
||||
else returning.mockResolvedValueOnce(result);
|
||||
expect(await setCommissionSlipProvider("slipok")).toMatchObject({ status: "error" });
|
||||
expect(refresh).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user