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 { commissionCheckouts, commissionTickets, users } from "@/db/schema";
|
||||||
import { AdminHeader } from "@/components/admin/admin-header";
|
import { AdminHeader } from "@/components/admin/admin-header";
|
||||||
import { CommissionLiveRefresh } from "@/components/commission/live-refresh";
|
import { CommissionLiveRefresh } from "@/components/commission/live-refresh";
|
||||||
|
import { AdminCommissionAvailabilityControl } from "@/components/commission/admin-availability-control";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { getAdminSession } from "@/lib/auth/server";
|
import { getAdminSession } from "@/lib/auth/server";
|
||||||
|
import { getCommissionAvailability } from "@/lib/commission/availability";
|
||||||
|
|
||||||
export const instant = false;
|
export const instant = false;
|
||||||
export default async function AdminCommissionPage({ searchParams }: PageProps<"/admin/commission">) {
|
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 query = await searchParams;
|
||||||
const rawPage = Number(query.page ?? 1);
|
const rawPage = Number(query.page ?? 1);
|
||||||
const page = Number.isInteger(rawPage) && rawPage > 0 ? Math.min(rawPage, 10000) : 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)
|
customer: users.name }).from(commissionTickets)
|
||||||
.innerJoin(commissionCheckouts, eq(commissionTickets.checkoutId, commissionCheckouts.id))
|
.innerJoin(commissionCheckouts, eq(commissionTickets.checkoutId, commissionCheckouts.id))
|
||||||
.innerJoin(users, eq(commissionTickets.userId, users.id))
|
.innerJoin(users, eq(commissionTickets.userId, users.id))
|
||||||
.orderBy(desc(commissionTickets.status), desc(commissionTickets.updatedAt), desc(commissionTickets.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" />
|
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">
|
<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>
|
<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.length === 0 && <p>ยังไม่มี Ticket</p>}
|
||||||
{tickets.slice(0, 50).map(({ ticket, amount, customer }) => <Link key={ticket.id} href={`/admin/commission/${ticket.id}`}>
|
{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>
|
<Card><CardHeader><CardTitle>{ticket.title || `#${ticket.id.slice(0, 8)}`} - {customer}</CardTitle>
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const requireCommissionUser = vi.fn();
|
||||||
|
const limitRequest = vi.fn();
|
||||||
|
const validateCommissionCatalog = vi.fn();
|
||||||
|
const promptPayConfig = vi.fn();
|
||||||
|
const transaction = vi.fn();
|
||||||
|
const forUpdate = vi.fn();
|
||||||
|
const returning = vi.fn();
|
||||||
|
const insert = vi.fn();
|
||||||
|
|
||||||
|
vi.mock("@/lib/commission/server", () => ({ requireCommissionUser }));
|
||||||
|
vi.mock("@/lib/security/rate-limit", () => ({ limitRequest }));
|
||||||
|
vi.mock("@/lib/commission/catalog", () => ({ validateCommissionCatalog }));
|
||||||
|
vi.mock("@/lib/commission/payment", () => ({ promptPayConfig }));
|
||||||
|
vi.mock("@/db", () => ({ getDb: () => ({ transaction }) }));
|
||||||
|
|
||||||
|
const { POST } = await import("./route");
|
||||||
|
|
||||||
|
function request() {
|
||||||
|
return new Request("https://guide.sudloh.com/api/commission/checkouts", {
|
||||||
|
method: "POST", headers: { Origin: "https://guide.sudloh.com", "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ teams: [], weapons: [{ characterKey: "c", weaponKeys: ["w1", "w2"] }],
|
||||||
|
constellations: [] }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("commission checkout availability", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
process.env.BETTER_AUTH_URL = "https://guide.sudloh.com";
|
||||||
|
vi.clearAllMocks();
|
||||||
|
requireCommissionUser.mockResolvedValue({ id: "customer-1" });
|
||||||
|
limitRequest.mockResolvedValue(undefined);
|
||||||
|
validateCommissionCatalog.mockResolvedValue(true);
|
||||||
|
promptPayConfig.mockReturnValue({ identifier: "test" });
|
||||||
|
transaction.mockImplementation((callback) => callback({
|
||||||
|
select: () => ({ from: () => ({ where: () => ({ for: forUpdate }) }) }),
|
||||||
|
insert,
|
||||||
|
}));
|
||||||
|
insert.mockReturnValue({ values: () => ({ returning }) });
|
||||||
|
returning.mockResolvedValue([{ id: "checkout-1" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a stale form submission after closure", async () => {
|
||||||
|
forUpdate.mockResolvedValue([{ isOpen: false }]);
|
||||||
|
const response = await POST(request());
|
||||||
|
expect(response.status).toBe(409);
|
||||||
|
expect(await response.json()).toEqual({ error: "commission-closed" });
|
||||||
|
expect(insert).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates a checkout while open", async () => {
|
||||||
|
forUpdate.mockResolvedValue([{ isOpen: true }]);
|
||||||
|
const response = await POST(request());
|
||||||
|
expect(response.status).toBe(201);
|
||||||
|
expect(await response.json()).toEqual({ checkoutId: "checkout-1" });
|
||||||
|
expect(insert).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { getDb } from "@/db";
|
import { getDb } from "@/db";
|
||||||
import { commissionCheckouts } from "@/db/schema";
|
import { commissionCheckouts, commissionSettings } from "@/db/schema";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
import { validateCommissionCatalog } from "@/lib/commission/catalog";
|
import { validateCommissionCatalog } from "@/lib/commission/catalog";
|
||||||
import { promptPayConfig } from "@/lib/commission/payment";
|
import { promptPayConfig } from "@/lib/commission/payment";
|
||||||
import { commissionPrice, commissionRequestSchema } from "@/lib/commission/request";
|
import { commissionPrice, commissionRequestSchema } from "@/lib/commission/request";
|
||||||
@@ -16,9 +17,16 @@ export async function POST(request: Request) {
|
|||||||
if (!parsed.success || !(await validateCommissionCatalog(parsed.data)))
|
if (!parsed.success || !(await validateCommissionCatalog(parsed.data)))
|
||||||
throw new HttpError(400, "invalid-commission-request");
|
throw new HttpError(400, "invalid-commission-request");
|
||||||
promptPayConfig();
|
promptPayConfig();
|
||||||
const [checkout] = await getDb().insert(commissionCheckouts).values({
|
const checkout = await getDb().transaction(async (tx) => {
|
||||||
userId: user.id, request: parsed.data, amountBaht: commissionPrice(parsed.data),
|
const [settings] = await tx.select({ isOpen: commissionSettings.isOpen }).from(commissionSettings)
|
||||||
}).returning({ id: commissionCheckouts.id });
|
.where(eq(commissionSettings.id, 1)).for("update");
|
||||||
|
if (!settings) throw new Error("Commission settings are missing. Apply the database migration.");
|
||||||
|
if (!settings.isOpen) throw new HttpError(409, "commission-closed");
|
||||||
|
const [created] = await tx.insert(commissionCheckouts).values({
|
||||||
|
userId: user.id, request: parsed.data, amountBaht: commissionPrice(parsed.data),
|
||||||
|
}).returning({ id: commissionCheckouts.id });
|
||||||
|
return created;
|
||||||
|
});
|
||||||
return Response.json({ checkoutId: checkout.id }, { status: 201, headers: { "Cache-Control": "no-store" } });
|
return Response.json({ checkoutId: checkout.id }, { status: 201, headers: { "Cache-Control": "no-store" } });
|
||||||
} catch (cause) { return errorResponse(cause); }
|
} catch (cause) { return errorResponse(cause); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,20 @@ import { CommissionSignOut } from "@/components/commission/sign-out";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { getCustomerSession } from "@/lib/auth/server";
|
import { getCustomerSession } from "@/lib/auth/server";
|
||||||
import { getCatalogOptions } from "@/lib/guides/queries";
|
import { getCatalogOptions } from "@/lib/guides/queries";
|
||||||
|
import { getCommissionAvailability } from "@/lib/commission/availability";
|
||||||
|
|
||||||
export const instant = false;
|
export const instant = false;
|
||||||
|
|
||||||
async function CommissionContent() {
|
async function CommissionContent() {
|
||||||
await connection();
|
await connection();
|
||||||
|
const availability = await getCommissionAvailability();
|
||||||
|
if (!availability.isOpen) return <div className="flex flex-col gap-4 rounded-xl border p-6">
|
||||||
|
<h2 className="font-heading text-xl font-semibold">ปิดรับ Commission ชั่วคราว</h2>
|
||||||
|
<p className="whitespace-pre-wrap">{availability.closureMessage}</p>
|
||||||
|
<Button variant="outline" nativeButton={false} render={<Link href="/commission/tickets" />}>
|
||||||
|
ดู Ticket และคำขอที่มีอยู่
|
||||||
|
</Button>
|
||||||
|
</div>;
|
||||||
const session = await getCustomerSession();
|
const session = await getCustomerSession();
|
||||||
if (!session) return <div className="flex flex-col gap-4 rounded-xl border p-6">
|
if (!session) return <div className="flex flex-col gap-4 rounded-xl border p-6">
|
||||||
<p>เข้าสู่ระบบหรือสมัครสมาชิกก่อนเริ่มคำขอ</p>
|
<p>เข้าสู่ระบบหรือสมัครสมาชิกก่อนเริ่มคำขอ</p>
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useTransition } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { setCommissionAvailability } from "@/app/admin/commission/actions";
|
||||||
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
|
||||||
|
export function AdminCommissionAvailabilityControl({ isOpen, closureMessage }: {
|
||||||
|
isOpen: boolean; closureMessage: string;
|
||||||
|
}) {
|
||||||
|
const router = useRouter();
|
||||||
|
const [message, setMessage] = useState(closureMessage);
|
||||||
|
const [pending, startTransition] = useTransition();
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
function save(nextOpen: boolean) {
|
||||||
|
setError("");
|
||||||
|
startTransition(async () => {
|
||||||
|
const result = await setCommissionAvailability(nextOpen, message);
|
||||||
|
if (result.status === "error") {
|
||||||
|
setError(result.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toast.success(nextOpen ? "เปิดรับ Commission แล้ว" : "ปิดรับ Commission แล้ว");
|
||||||
|
router.refresh();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Card><CardHeader><div className="flex items-center gap-3">
|
||||||
|
<CardTitle>การรับ Commission</CardTitle><Badge variant={isOpen ? "default" : "secondary"}>
|
||||||
|
{isOpen ? "เปิดรับ" : "ปิดรับ"}</Badge></div>
|
||||||
|
<CardDescription>การปิดรับจะหยุดคำขอใหม่ ลูกค้าที่มีคำขออยู่แล้วชำระเงินต่อได้</CardDescription>
|
||||||
|
</CardHeader><CardContent className="flex flex-col gap-3">
|
||||||
|
<label htmlFor="commission-closure-message" className="text-sm font-medium">ข้อความเมื่อปิดรับ</label>
|
||||||
|
<Textarea id="commission-closure-message" value={message} maxLength={1000}
|
||||||
|
onChange={(event) => setMessage(event.target.value)} disabled={pending}
|
||||||
|
placeholder="แจ้งเหตุผลที่ปิดรับและข้อมูลที่ลูกค้าควรทราบ" />
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Button type="button" disabled={pending || !message.trim()} onClick={() => save(false)}>
|
||||||
|
{isOpen ? "ปิดรับ Commission" : "บันทึกข้อความ"}
|
||||||
|
</Button>
|
||||||
|
{!isOpen && <Button type="button" variant="outline" disabled={pending} onClick={() => save(true)}>
|
||||||
|
เปิดรับ Commission
|
||||||
|
</Button>}
|
||||||
|
</div>
|
||||||
|
{error && <Alert variant="destructive"><AlertDescription>{error}</AlertDescription></Alert>}
|
||||||
|
</CardContent></Card>;
|
||||||
|
}
|
||||||
@@ -47,6 +47,7 @@ function checkoutError(code: string | undefined, status: number): string {
|
|||||||
code === "COMMISSION_PROMPTPAY_VALUE-not-configured")
|
code === "COMMISSION_PROMPTPAY_VALUE-not-configured")
|
||||||
return "ระบบยังตั้งค่าบัญชี PromptPay ไม่ถูกต้อง กรุณาแจ้งผู้ดูแลระบบ";
|
return "ระบบยังตั้งค่าบัญชี PromptPay ไม่ถูกต้อง กรุณาแจ้งผู้ดูแลระบบ";
|
||||||
if (code === "invalid-commission-request") return "รายการที่เลือกไม่ตรงกับข้อมูลล่าสุด กรุณาโหลดหน้าใหม่แล้วเลือกอีกครั้ง";
|
if (code === "invalid-commission-request") return "รายการที่เลือกไม่ตรงกับข้อมูลล่าสุด กรุณาโหลดหน้าใหม่แล้วเลือกอีกครั้ง";
|
||||||
|
if (code === "commission-closed") return "ขณะนี้ปิดรับ Commission ใหม่ กรุณาโหลดหน้าเพื่อดูประกาศ";
|
||||||
return "สร้างคำขอไม่สำเร็จ กรุณาลองอีกครั้ง";
|
return "สร้างคำขอไม่สำเร็จ กรุณาลองอีกครั้ง";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -162,6 +162,13 @@ export const commissionCheckouts = commissionSchema.table("checkout", {
|
|||||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
||||||
}, (table) => [index("commission_checkout_user_idx").on(table.userId, table.createdAt)]);
|
}, (table) => [index("commission_checkout_user_idx").on(table.userId, table.createdAt)]);
|
||||||
|
|
||||||
|
export const commissionSettings = commissionSchema.table("settings", {
|
||||||
|
id: integer("id").primaryKey(),
|
||||||
|
isOpen: boolean("is_open").default(true).notNull(),
|
||||||
|
closureMessage: text("closure_message").default("").notNull(),
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
export const commissionTickets = commissionSchema.table("ticket", {
|
export const commissionTickets = commissionSchema.table("ticket", {
|
||||||
id: uuid("id").defaultRandom().primaryKey(),
|
id: uuid("id").defaultRandom().primaryKey(),
|
||||||
checkoutId: uuid("checkout_id").notNull().references(() => commissionCheckouts.id),
|
checkoutId: uuid("checkout_id").notNull().references(() => commissionCheckouts.id),
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
CREATE TABLE "commission"."settings" (
|
||||||
|
"id" integer PRIMARY KEY NOT NULL,
|
||||||
|
"is_open" boolean DEFAULT true NOT NULL,
|
||||||
|
"closure_message" text DEFAULT '' NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
INSERT INTO "commission"."settings" ("id", "is_open", "closure_message") VALUES (1, true, '');
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -470,6 +470,13 @@
|
|||||||
"when": 1790849456239,
|
"when": 1790849456239,
|
||||||
"tag": "0066_burly_william_stryker",
|
"tag": "0066_burly_william_stryker",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 67,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1790863872049,
|
||||||
|
"tag": "0067_nervous_psylocke",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import "server-only";
|
||||||
|
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { getDb } from "@/db";
|
||||||
|
import { commissionSettings } from "@/db/schema";
|
||||||
|
|
||||||
|
export async function getCommissionAvailability() {
|
||||||
|
const [settings] = await getDb().select({
|
||||||
|
isOpen: commissionSettings.isOpen,
|
||||||
|
closureMessage: commissionSettings.closureMessage,
|
||||||
|
}).from(commissionSettings).where(eq(commissionSettings.id, 1)).limit(1);
|
||||||
|
if (!settings) throw new Error("Commission settings are missing. Apply the database migration.");
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user