diff --git a/.env.example b/.env.example
index c7a8b05..72ce47f 100644
--- a/.env.example
+++ b/.env.example
@@ -59,6 +59,9 @@ COMMISSION_RECEIVER_ACCOUNT_NUMBER=0812345678
# Use the complete verification URL supplied by your Slip2Go API Connect account.
SLIP2GO_VERIFY_URL=https://your-slip2go-api-host/api/verify-slip/qr-image/info
SLIP2GO_API_SECRET=replace-with-slip2go-secret
+# Configure the SlipOK LINE LIFF branch with the commission receiving account before switching.
+SLIPOK_BRANCH_ID=
+SLIPOK_API_KEY=
# Stable across replicas. Generate once and store only in the external secret.
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY=replace-with-a-stable-32-byte-base64-key
diff --git a/app/admin/commission/actions.test.ts b/app/admin/commission/actions.test.ts
index 9329c0b..26a7d6d 100644
--- a/app/admin/commission/actions.test.ts
+++ b/app/admin/commission/actions.test.ts
@@ -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");
diff --git a/app/admin/commission/actions.ts b/app/admin/commission/actions.ts
index f3f3f4f..6321488 100644
--- a/app/admin/commission/actions.ts
+++ b/app/admin/commission/actions.ts
@@ -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 };
+}
diff --git a/app/admin/commission/page.tsx b/app/admin/commission/page.tsx
index b5707d1..6c48a96 100644
--- a/app/admin/commission/page.tsx
+++ b/app/admin/commission/page.tsx
@@ -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
Commission tickets
-
}>ดูรายได้ประจำเดือน
+
+
+
}>ดูรายได้ประจำเดือน
+
diff --git a/app/admin/commission/slip-provider-actions.test.ts b/app/admin/commission/slip-provider-actions.test.ts
new file mode 100644
index 0000000..66ffbc8
--- /dev/null
+++ b/app/admin/commission/slip-provider-actions.test.ts
@@ -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();
+ });
+});
diff --git a/components/commission/admin-slip-provider-control.tsx b/components/commission/admin-slip-provider-control.tsx
new file mode 100644
index 0000000..9071d7a
--- /dev/null
+++ b/components/commission/admin-slip-provider-control.tsx
@@ -0,0 +1,69 @@
+"use client";
+
+import { useState, useTransition } from "react";
+import { toast } from "sonner";
+import { setCommissionSlipProvider } from "@/app/admin/commission/actions";
+import type { SlipProvider } from "@/lib/commission/slip-provider";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { Button } from "@/components/ui/button";
+import { Settings } from "lucide-react";
+import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
+import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";
+import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
+
+export function AdminCommissionSlipProviderControl({ provider, configured }: {
+ provider: SlipProvider; configured: Record;
+}) {
+ const [open, setOpen] = useState(false);
+ const [selected, setSelected] = useState(provider);
+ const [pending, startTransition] = useTransition();
+ const [error, setError] = useState("");
+
+ function save() {
+ setError("");
+ startTransition(async () => {
+ try {
+ const result = await setCommissionSlipProvider(selected);
+ if (result.status === "error") { setError(result.message); return; }
+ setOpen(false);
+ toast.success("บันทึกผู้ให้บริการตรวจสลิปแล้ว");
+ } catch { setError("บันทึกไม่สำเร็จ กรุณาลองอีกครั้งหรือโหลดหน้าใหม่"); }
+ });
+ }
+
+ return ;
+}
diff --git a/db/schema.ts b/db/schema.ts
index e51600e..2fbdad0 100644
--- a/db/schema.ts
+++ b/db/schema.ts
@@ -170,8 +170,9 @@ export const commissionSettings = commissionSchema.table("settings", {
id: integer("id").primaryKey(),
isOpen: boolean("is_open").default(true).notNull(),
closureMessage: text("closure_message").default("").notNull(),
+ slipProvider: varchar("slip_provider", { length: 16 }).$type<"slip2go" | "slipok">().default("slip2go").notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
-});
+}, (table) => [check("commission_settings_slip_provider_check", sql`${table.slipProvider} in ('slip2go', 'slipok')`)]);
export const commissionTickets = commissionSchema.table("ticket", {
id: uuid("id").defaultRandom().primaryKey(),
@@ -191,6 +192,7 @@ export const commissionPayments = commissionSchema.table("payment", {
id: uuid("id").defaultRandom().primaryKey(),
checkoutId: uuid("checkout_id").notNull().references(() => commissionCheckouts.id),
ticketId: uuid("ticket_id").notNull().references(() => commissionTickets.id),
+ provider: varchar("provider", { length: 16 }).$type<"slip2go" | "slipok">().default("slip2go").notNull(),
referenceId: text("reference_id").notNull(),
transRef: text("trans_ref").notNull(),
amountBaht: integer("amount_baht").notNull(),
@@ -200,6 +202,7 @@ export const commissionPayments = commissionSchema.table("payment", {
providerData: jsonb("provider_data").$type>().notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
}, (table) => [
+ check("commission_payment_provider_check", sql`${table.provider} in ('slip2go', 'slipok')`),
uniqueIndex("commission_payment_checkout_unique").on(table.checkoutId),
uniqueIndex("commission_payment_reference_unique").on(table.referenceId),
uniqueIndex("commission_payment_trans_ref_unique").on(table.transRef),
diff --git a/drizzle/0080_wet_colonel_america.sql b/drizzle/0080_wet_colonel_america.sql
new file mode 100644
index 0000000..0095cc6
--- /dev/null
+++ b/drizzle/0080_wet_colonel_america.sql
@@ -0,0 +1,4 @@
+ALTER TABLE "commission"."payment" ADD COLUMN "provider" varchar(16) DEFAULT 'slip2go' NOT NULL;--> statement-breakpoint
+ALTER TABLE "commission"."settings" ADD COLUMN "slip_provider" varchar(16) DEFAULT 'slip2go' NOT NULL;--> statement-breakpoint
+ALTER TABLE "commission"."payment" ADD CONSTRAINT "commission_payment_provider_check" CHECK ("commission"."payment"."provider" in ('slip2go', 'slipok'));--> statement-breakpoint
+ALTER TABLE "commission"."settings" ADD CONSTRAINT "commission_settings_slip_provider_check" CHECK ("commission"."settings"."slip_provider" in ('slip2go', 'slipok'));
\ No newline at end of file
diff --git a/drizzle/meta/0080_snapshot.json b/drizzle/meta/0080_snapshot.json
new file mode 100644
index 0000000..c297e5a
--- /dev/null
+++ b/drizzle/meta/0080_snapshot.json
@@ -0,0 +1,5202 @@
+{
+ "id": "4b61fd31-af62-4937-96f3-1e3bcaf494f1",
+ "prevId": "4a884ac3-555c-42cc-8d49-4fec960a7c0d",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "auth.account": {
+ "name": "account",
+ "schema": "auth",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "issuer": {
+ "name": "issuer",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token_expires_at": {
+ "name": "access_token_expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token_expires_at": {
+ "name": "refresh_token_expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "account_provider_unique": {
+ "name": "account_provider_unique",
+ "columns": [
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "account_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "account_user_id_idx": {
+ "name": "account_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "account_user_id_user_id_fk": {
+ "name": "account_user_id_user_id_fk",
+ "tableFrom": "account",
+ "tableTo": "user",
+ "schemaTo": "auth",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "guides.artifact_condition": {
+ "name": "artifact_condition",
+ "schema": "guides",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "guide_id": {
+ "name": "guide_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(120)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "note": {
+ "name": "note",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "icon_media_id": {
+ "name": "icon_media_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ }
+ },
+ "indexes": {
+ "artifact_condition_order_idx": {
+ "name": "artifact_condition_order_idx",
+ "columns": [
+ {
+ "expression": "guide_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sort_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "artifact_condition_guide_id_guide_id_fk": {
+ "name": "artifact_condition_guide_id_guide_id_fk",
+ "tableFrom": "artifact_condition",
+ "tableTo": "guide",
+ "schemaTo": "guides",
+ "columnsFrom": [
+ "guide_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "artifact_condition_icon_media_id_media_id_fk": {
+ "name": "artifact_condition_icon_media_id_media_id_fk",
+ "tableFrom": "artifact_condition",
+ "tableTo": "media",
+ "columnsFrom": [
+ "icon_media_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "guides.artifact_group": {
+ "name": "artifact_group",
+ "schema": "guides",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "condition_id": {
+ "name": "condition_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ }
+ },
+ "indexes": {
+ "artifact_group_order_idx": {
+ "name": "artifact_group_order_idx",
+ "columns": [
+ {
+ "expression": "condition_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sort_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "artifact_group_condition_id_artifact_condition_id_fk": {
+ "name": "artifact_group_condition_id_artifact_condition_id_fk",
+ "tableFrom": "artifact_group",
+ "tableTo": "artifact_condition",
+ "schemaTo": "guides",
+ "columnsFrom": [
+ "condition_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "guides.artifact_profile": {
+ "name": "artifact_profile",
+ "schema": "guides",
+ "columns": {
+ "guide_id": {
+ "name": "guide_id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "sands": {
+ "name": "sands",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "goblet": {
+ "name": "goblet",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "circlet": {
+ "name": "circlet",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "substats": {
+ "name": "substats",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "recommended_conditions": {
+ "name": "recommended_conditions",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "targets": {
+ "name": "targets",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "target_conditions": {
+ "name": "target_conditions",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "talents": {
+ "name": "talents",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "note": {
+ "name": "note",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "artifact_profile_guide_id_guide_id_fk": {
+ "name": "artifact_profile_guide_id_guide_id_fk",
+ "tableFrom": "artifact_profile",
+ "tableTo": "guide",
+ "schemaTo": "guides",
+ "columnsFrom": [
+ "guide_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "guides.artifact_recommendation": {
+ "name": "artifact_recommendation",
+ "schema": "guides",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "group_id": {
+ "name": "group_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "first_artifact_key": {
+ "name": "first_artifact_key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "second_artifact_key": {
+ "name": "second_artifact_key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "percent": {
+ "name": "percent",
+ "type": "numeric(8, 2)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "note": {
+ "name": "note",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ }
+ },
+ "indexes": {
+ "artifact_recommendation_order_idx": {
+ "name": "artifact_recommendation_order_idx",
+ "columns": [
+ {
+ "expression": "group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sort_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "artifact_recommendation_group_id_artifact_group_id_fk": {
+ "name": "artifact_recommendation_group_id_artifact_group_id_fk",
+ "tableFrom": "artifact_recommendation",
+ "tableTo": "artifact_group",
+ "schemaTo": "guides",
+ "columnsFrom": [
+ "group_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.audit_log": {
+ "name": "audit_log",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "bigint",
+ "primaryKey": true,
+ "notNull": true,
+ "identity": {
+ "type": "always",
+ "name": "audit_log_id_seq",
+ "schema": "public",
+ "increment": "1",
+ "startWith": "1",
+ "minValue": "1",
+ "maxValue": "9223372036854775807",
+ "cache": "1",
+ "cycle": false
+ }
+ },
+ "author_id": {
+ "name": "author_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "author": {
+ "name": "author",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "text": {
+ "name": "text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "details": {
+ "name": "details",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'::jsonb"
+ },
+ "time": {
+ "name": "time",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "audit_log_time_idx": {
+ "name": "audit_log_time_idx",
+ "columns": [
+ {
+ "expression": "time",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audit_log_author_time_idx": {
+ "name": "audit_log_author_time_idx",
+ "columns": [
+ {
+ "expression": "author_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "time",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "catalog.artifact": {
+ "name": "artifact",
+ "schema": "catalog",
+ "columns": {
+ "key": {
+ "name": "key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "rarity": {
+ "name": "rarity",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "image_key": {
+ "name": "image_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "catalog_artifact_name_idx": {
+ "name": "catalog_artifact_name_idx",
+ "columns": [
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "artifact_key_pk": {
+ "name": "artifact_key_pk",
+ "columns": [
+ "key"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "catalog.character": {
+ "name": "character",
+ "schema": "catalog",
+ "columns": {
+ "key": {
+ "name": "key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "element": {
+ "name": "element",
+ "type": "varchar(48)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rarity": {
+ "name": "rarity",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "weapon_type": {
+ "name": "weapon_type",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "image_key": {
+ "name": "image_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "element_icon_key": {
+ "name": "element_icon_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "coop_image_key": {
+ "name": "coop_image_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name_card_image_key": {
+ "name": "name_card_image_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "constellations": {
+ "name": "constellations",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "talents": {
+ "name": "talents",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "ascension": {
+ "name": "ascension",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "leveling": {
+ "name": "leveling",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "base_stats": {
+ "name": "base_stats",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ }
+ },
+ "indexes": {
+ "catalog_character_name_idx": {
+ "name": "catalog_character_name_idx",
+ "columns": [
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "character_key_pk": {
+ "name": "character_key_pk",
+ "columns": [
+ "key"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "catalog.material": {
+ "name": "material",
+ "schema": "catalog",
+ "columns": {
+ "key": {
+ "name": "key",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rarity": {
+ "name": "rarity",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "type": {
+ "name": "type",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "image_key": {
+ "name": "image_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "catalog_material_name_idx": {
+ "name": "catalog_material_name_idx",
+ "columns": [
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "catalog.weapon": {
+ "name": "weapon",
+ "schema": "catalog",
+ "columns": {
+ "key": {
+ "name": "key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "rarity": {
+ "name": "rarity",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "weapon_type": {
+ "name": "weapon_type",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "image_key": {
+ "name": "image_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "catalog_weapon_name_idx": {
+ "name": "catalog_weapon_name_idx",
+ "columns": [
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "weapon_key_pk": {
+ "name": "weapon_key_pk",
+ "columns": [
+ "key"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "comments.attachment": {
+ "name": "attachment",
+ "schema": "comments",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "comment_id": {
+ "name": "comment_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "object_key": {
+ "name": "object_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "mime_type": {
+ "name": "mime_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "byte_size": {
+ "name": "byte_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "comment_attachment_key_unique": {
+ "name": "comment_attachment_key_unique",
+ "columns": [
+ {
+ "expression": "object_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "comment_attachment_comment_idx": {
+ "name": "comment_attachment_comment_idx",
+ "columns": [
+ {
+ "expression": "comment_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "attachment_comment_id_comment_id_fk": {
+ "name": "attachment_comment_id_comment_id_fk",
+ "tableFrom": "attachment",
+ "tableTo": "comment",
+ "schemaTo": "comments",
+ "columnsFrom": [
+ "comment_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "comment_image_size": {
+ "name": "comment_image_size",
+ "value": "\"comments\".\"attachment\".\"byte_size\" > 0 and \"comments\".\"attachment\".\"byte_size\" <= 10485760"
+ },
+ "comment_image_type": {
+ "name": "comment_image_type",
+ "value": "\"comments\".\"attachment\".\"mime_type\" in ('image/jpeg', 'image/png', 'image/webp')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "comments.push_subscription": {
+ "name": "push_subscription",
+ "schema": "comments",
+ "columns": {
+ "endpoint": {
+ "name": "endpoint",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "p256dh": {
+ "name": "p256dh",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "auth": {
+ "name": "auth",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "comment_push_subscription_user_idx": {
+ "name": "comment_push_subscription_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "push_subscription_user_id_user_id_fk": {
+ "name": "push_subscription_user_id_user_id_fk",
+ "tableFrom": "push_subscription",
+ "tableTo": "user",
+ "schemaTo": "auth",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "comments.reaction": {
+ "name": "reaction",
+ "schema": "comments",
+ "columns": {
+ "comment_id": {
+ "name": "comment_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "reaction_comment_id_comment_id_fk": {
+ "name": "reaction_comment_id_comment_id_fk",
+ "tableFrom": "reaction",
+ "tableTo": "comment",
+ "schemaTo": "comments",
+ "columnsFrom": [
+ "comment_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "reaction_user_id_user_id_fk": {
+ "name": "reaction_user_id_user_id_fk",
+ "tableFrom": "reaction",
+ "tableTo": "user",
+ "schemaTo": "auth",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "reaction_comment_id_user_id_pk": {
+ "name": "reaction_comment_id_user_id_pk",
+ "columns": [
+ "comment_id",
+ "user_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "comment_reaction_value": {
+ "name": "comment_reaction_value",
+ "value": "\"comments\".\"reaction\".\"value\" in (-1, 1)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "comments.read_state": {
+ "name": "read_state",
+ "schema": "comments",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target": {
+ "name": "target",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "seen_at": {
+ "name": "seen_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "read_state_user_id_user_id_fk": {
+ "name": "read_state_user_id_user_id_fk",
+ "tableFrom": "read_state",
+ "tableTo": "user",
+ "schemaTo": "auth",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "read_state_user_id_target_pk": {
+ "name": "read_state_user_id_target_pk",
+ "columns": [
+ "user_id",
+ "target"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "comments.revision_attachment": {
+ "name": "revision_attachment",
+ "schema": "comments",
+ "columns": {
+ "revision_id": {
+ "name": "revision_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "attachment_id": {
+ "name": "attachment_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "position": {
+ "name": "position",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "comment_revision_attachment_position_unique": {
+ "name": "comment_revision_attachment_position_unique",
+ "columns": [
+ {
+ "expression": "revision_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "position",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "revision_attachment_revision_id_revision_id_fk": {
+ "name": "revision_attachment_revision_id_revision_id_fk",
+ "tableFrom": "revision_attachment",
+ "tableTo": "revision",
+ "schemaTo": "comments",
+ "columnsFrom": [
+ "revision_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "revision_attachment_attachment_id_attachment_id_fk": {
+ "name": "revision_attachment_attachment_id_attachment_id_fk",
+ "tableFrom": "revision_attachment",
+ "tableTo": "attachment",
+ "schemaTo": "comments",
+ "columnsFrom": [
+ "attachment_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "revision_attachment_revision_id_attachment_id_pk": {
+ "name": "revision_attachment_revision_id_attachment_id_pk",
+ "columns": [
+ "revision_id",
+ "attachment_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "comment_attachment_position": {
+ "name": "comment_attachment_position",
+ "value": "\"comments\".\"revision_attachment\".\"position\" >= 0 and \"comments\".\"revision_attachment\".\"position\" < 5"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "comments.revision": {
+ "name": "revision",
+ "schema": "comments",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "comment_id": {
+ "name": "comment_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "text": {
+ "name": "text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "comment_revision_version_unique": {
+ "name": "comment_revision_version_unique",
+ "columns": [
+ {
+ "expression": "comment_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "version",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "revision_comment_id_comment_id_fk": {
+ "name": "revision_comment_id_comment_id_fk",
+ "tableFrom": "revision",
+ "tableTo": "comment",
+ "schemaTo": "comments",
+ "columnsFrom": [
+ "comment_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "comment_text_length": {
+ "name": "comment_text_length",
+ "value": "length(\"comments\".\"revision\".\"text\") <= 4000"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "comments.thread": {
+ "name": "thread",
+ "schema": "comments",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "guide_id": {
+ "name": "guide_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "schedule_id": {
+ "name": "schedule_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "comment_thread_guide_unique": {
+ "name": "comment_thread_guide_unique",
+ "columns": [
+ {
+ "expression": "guide_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "comment_thread_schedule_unique": {
+ "name": "comment_thread_schedule_unique",
+ "columns": [
+ {
+ "expression": "schedule_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "thread_guide_id_guide_id_fk": {
+ "name": "thread_guide_id_guide_id_fk",
+ "tableFrom": "thread",
+ "tableTo": "guide",
+ "schemaTo": "guides",
+ "columnsFrom": [
+ "guide_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "thread_schedule_id_stygian_schedule_schedule_id_fk": {
+ "name": "thread_schedule_id_stygian_schedule_schedule_id_fk",
+ "tableFrom": "thread",
+ "tableTo": "stygian_schedule",
+ "schemaTo": "guides",
+ "columnsFrom": [
+ "schedule_id"
+ ],
+ "columnsTo": [
+ "schedule_id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "comment_thread_one_target": {
+ "name": "comment_thread_one_target",
+ "value": "(\"comments\".\"thread\".\"guide_id\" is null) <> (\"comments\".\"thread\".\"schedule_id\" is null)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "comments.comment": {
+ "name": "comment",
+ "schema": "comments",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "thread_id": {
+ "name": "thread_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "author_id": {
+ "name": "author_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "root_id": {
+ "name": "root_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reply_to_id": {
+ "name": "reply_to_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "hidden": {
+ "name": "hidden",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "hearted_by_id": {
+ "name": "hearted_by_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "comment_thread_id_unique": {
+ "name": "comment_thread_id_unique",
+ "columns": [
+ {
+ "expression": "thread_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "comment_thread_root_time_idx": {
+ "name": "comment_thread_root_time_idx",
+ "columns": [
+ {
+ "expression": "thread_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "root_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "comment_root_time_idx": {
+ "name": "comment_root_time_idx",
+ "columns": [
+ {
+ "expression": "root_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "comment_inbox_time_idx": {
+ "name": "comment_inbox_time_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "comment_thread_id_thread_id_fk": {
+ "name": "comment_thread_id_thread_id_fk",
+ "tableFrom": "comment",
+ "tableTo": "thread",
+ "schemaTo": "comments",
+ "columnsFrom": [
+ "thread_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "comment_author_id_user_id_fk": {
+ "name": "comment_author_id_user_id_fk",
+ "tableFrom": "comment",
+ "tableTo": "user",
+ "schemaTo": "auth",
+ "columnsFrom": [
+ "author_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "comment_hearted_by_id_user_id_fk": {
+ "name": "comment_hearted_by_id_user_id_fk",
+ "tableFrom": "comment",
+ "tableTo": "user",
+ "schemaTo": "auth",
+ "columnsFrom": [
+ "hearted_by_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "comment_thread_id_root_id_comment_thread_id_id_fk": {
+ "name": "comment_thread_id_root_id_comment_thread_id_id_fk",
+ "tableFrom": "comment",
+ "tableTo": "comment",
+ "schemaTo": "comments",
+ "columnsFrom": [
+ "thread_id",
+ "root_id"
+ ],
+ "columnsTo": [
+ "thread_id",
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "comment_thread_id_reply_to_id_comment_thread_id_id_fk": {
+ "name": "comment_thread_id_reply_to_id_comment_thread_id_id_fk",
+ "tableFrom": "comment",
+ "tableTo": "comment",
+ "schemaTo": "comments",
+ "columnsFrom": [
+ "thread_id",
+ "reply_to_id"
+ ],
+ "columnsTo": [
+ "thread_id",
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "comment_reply_shape": {
+ "name": "comment_reply_shape",
+ "value": "(\"comments\".\"comment\".\"root_id\" is null) = (\"comments\".\"comment\".\"reply_to_id\" is null)"
+ },
+ "comment_version_positive": {
+ "name": "comment_version_positive",
+ "value": "\"comments\".\"comment\".\"version\" > 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "commission.checkout": {
+ "name": "checkout",
+ "schema": "commission",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "request": {
+ "name": "request",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "amount_baht": {
+ "name": "amount_baht",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "commission_checkout_user_idx": {
+ "name": "commission_checkout_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "checkout_user_id_user_id_fk": {
+ "name": "checkout_user_id_user_id_fk",
+ "tableFrom": "checkout",
+ "tableTo": "user",
+ "schemaTo": "auth",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "commission.message": {
+ "name": "message",
+ "schema": "commission",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "ticket_id": {
+ "name": "ticket_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "author_id": {
+ "name": "author_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reply_to_id": {
+ "name": "reply_to_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "text": {
+ "name": "text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "image_object_key": {
+ "name": "image_object_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "image_mime_type": {
+ "name": "image_mime_type",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "image_object_keys": {
+ "name": "image_object_keys",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "commission_message_ticket_time_idx": {
+ "name": "commission_message_ticket_time_idx",
+ "columns": [
+ {
+ "expression": "ticket_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "commission_message_reply_idx": {
+ "name": "commission_message_reply_idx",
+ "columns": [
+ {
+ "expression": "reply_to_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "message_ticket_id_ticket_id_fk": {
+ "name": "message_ticket_id_ticket_id_fk",
+ "tableFrom": "message",
+ "tableTo": "ticket",
+ "schemaTo": "commission",
+ "columnsFrom": [
+ "ticket_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "message_author_id_user_id_fk": {
+ "name": "message_author_id_user_id_fk",
+ "tableFrom": "message",
+ "tableTo": "user",
+ "schemaTo": "auth",
+ "columnsFrom": [
+ "author_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "message_reply_to_id_message_id_fk": {
+ "name": "message_reply_to_id_message_id_fk",
+ "tableFrom": "message",
+ "tableTo": "message",
+ "schemaTo": "commission",
+ "columnsFrom": [
+ "reply_to_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "commission.payment": {
+ "name": "payment",
+ "schema": "commission",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "checkout_id": {
+ "name": "checkout_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ticket_id": {
+ "name": "ticket_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'slip2go'"
+ },
+ "reference_id": {
+ "name": "reference_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "trans_ref": {
+ "name": "trans_ref",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "amount_baht": {
+ "name": "amount_baht",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "transferred_at": {
+ "name": "transferred_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slip_object_key": {
+ "name": "slip_object_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slip_mime_type": {
+ "name": "slip_mime_type",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_data": {
+ "name": "provider_data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "commission_payment_checkout_unique": {
+ "name": "commission_payment_checkout_unique",
+ "columns": [
+ {
+ "expression": "checkout_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "commission_payment_reference_unique": {
+ "name": "commission_payment_reference_unique",
+ "columns": [
+ {
+ "expression": "reference_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "commission_payment_trans_ref_unique": {
+ "name": "commission_payment_trans_ref_unique",
+ "columns": [
+ {
+ "expression": "trans_ref",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "commission_payment_transferred_at_idx": {
+ "name": "commission_payment_transferred_at_idx",
+ "columns": [
+ {
+ "expression": "transferred_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "payment_checkout_id_checkout_id_fk": {
+ "name": "payment_checkout_id_checkout_id_fk",
+ "tableFrom": "payment",
+ "tableTo": "checkout",
+ "schemaTo": "commission",
+ "columnsFrom": [
+ "checkout_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "payment_ticket_id_ticket_id_fk": {
+ "name": "payment_ticket_id_ticket_id_fk",
+ "tableFrom": "payment",
+ "tableTo": "ticket",
+ "schemaTo": "commission",
+ "columnsFrom": [
+ "ticket_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "commission_payment_provider_check": {
+ "name": "commission_payment_provider_check",
+ "value": "\"commission\".\"payment\".\"provider\" in ('slip2go', 'slipok')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "commission.push_subscription": {
+ "name": "push_subscription",
+ "schema": "commission",
+ "columns": {
+ "endpoint": {
+ "name": "endpoint",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scope": {
+ "name": "scope",
+ "type": "varchar(8)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "p256dh": {
+ "name": "p256dh",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "auth": {
+ "name": "auth",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "commission_push_subscription_user_idx": {
+ "name": "commission_push_subscription_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "push_subscription_user_id_user_id_fk": {
+ "name": "push_subscription_user_id_user_id_fk",
+ "tableFrom": "push_subscription",
+ "tableTo": "user",
+ "schemaTo": "auth",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "commission.reaction": {
+ "name": "reaction",
+ "schema": "commission",
+ "columns": {
+ "message_id": {
+ "name": "message_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "emoji": {
+ "name": "emoji",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "reaction_message_id_message_id_fk": {
+ "name": "reaction_message_id_message_id_fk",
+ "tableFrom": "reaction",
+ "tableTo": "message",
+ "schemaTo": "commission",
+ "columnsFrom": [
+ "message_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "reaction_user_id_user_id_fk": {
+ "name": "reaction_user_id_user_id_fk",
+ "tableFrom": "reaction",
+ "tableTo": "user",
+ "schemaTo": "auth",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "reaction_message_id_user_id_emoji_pk": {
+ "name": "reaction_message_id_user_id_emoji_pk",
+ "columns": [
+ "message_id",
+ "user_id",
+ "emoji"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "commission.settings": {
+ "name": "settings",
+ "schema": "commission",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "is_open": {
+ "name": "is_open",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "closure_message": {
+ "name": "closure_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "slip_provider": {
+ "name": "slip_provider",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'slip2go'"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "commission_settings_slip_provider_check": {
+ "name": "commission_settings_slip_provider_check",
+ "value": "\"commission\".\"settings\".\"slip_provider\" in ('slip2go', 'slipok')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "commission.ticket": {
+ "name": "ticket",
+ "schema": "commission",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "checkout_id": {
+ "name": "checkout_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(80)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'open'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "commission_ticket_checkout_unique": {
+ "name": "commission_ticket_checkout_unique",
+ "columns": [
+ {
+ "expression": "checkout_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "commission_ticket_user_idx": {
+ "name": "commission_ticket_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "commission_ticket_status_idx": {
+ "name": "commission_ticket_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "ticket_checkout_id_checkout_id_fk": {
+ "name": "ticket_checkout_id_checkout_id_fk",
+ "tableFrom": "ticket",
+ "tableTo": "checkout",
+ "schemaTo": "commission",
+ "columnsFrom": [
+ "checkout_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "ticket_user_id_user_id_fk": {
+ "name": "ticket_user_id_user_id_fk",
+ "tableFrom": "ticket",
+ "tableTo": "user",
+ "schemaTo": "auth",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "guides.constellation_condition": {
+ "name": "constellation_condition",
+ "schema": "guides",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "guide_id": {
+ "name": "guide_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(120)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "note": {
+ "name": "note",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "icon_media_id": {
+ "name": "icon_media_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ }
+ },
+ "indexes": {
+ "constellation_condition_order_idx": {
+ "name": "constellation_condition_order_idx",
+ "columns": [
+ {
+ "expression": "guide_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sort_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "constellation_condition_guide_id_guide_id_fk": {
+ "name": "constellation_condition_guide_id_guide_id_fk",
+ "tableFrom": "constellation_condition",
+ "tableTo": "guide",
+ "schemaTo": "guides",
+ "columnsFrom": [
+ "guide_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "constellation_condition_icon_media_id_media_id_fk": {
+ "name": "constellation_condition_icon_media_id_media_id_fk",
+ "tableFrom": "constellation_condition",
+ "tableTo": "media",
+ "columnsFrom": [
+ "icon_media_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "guides.constellation_recommendation": {
+ "name": "constellation_recommendation",
+ "schema": "guides",
+ "columns": {
+ "condition_id": {
+ "name": "condition_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "level": {
+ "name": "level",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "detail": {
+ "name": "detail",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "cumulative_percent": {
+ "name": "cumulative_percent",
+ "type": "numeric(8, 2)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_great": {
+ "name": "is_great",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "constellation_recommendation_condition_id_constellation_condition_id_fk": {
+ "name": "constellation_recommendation_condition_id_constellation_condition_id_fk",
+ "tableFrom": "constellation_recommendation",
+ "tableTo": "constellation_condition",
+ "schemaTo": "guides",
+ "columnsFrom": [
+ "condition_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "constellation_recommendation_condition_id_level_pk": {
+ "name": "constellation_recommendation_condition_id_level_pk",
+ "columns": [
+ "condition_id",
+ "level"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "guides.extra_section_row": {
+ "name": "extra_section_row",
+ "schema": "guides",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "section_id": {
+ "name": "section_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "media_id": {
+ "name": "media_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "heading": {
+ "name": "heading",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "body": {
+ "name": "body",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "percent": {
+ "name": "percent",
+ "type": "numeric(8, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "background_color": {
+ "name": "background_color",
+ "type": "varchar(7)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "text_color": {
+ "name": "text_color",
+ "type": "varchar(7)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "highlights": {
+ "name": "highlights",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "catalog_items": {
+ "name": "catalog_items",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "sheet": {
+ "name": "sheet",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ }
+ },
+ "indexes": {
+ "extra_section_row_order_idx": {
+ "name": "extra_section_row_order_idx",
+ "columns": [
+ {
+ "expression": "section_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sort_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "extra_section_row_section_id_extra_section_section_id_fk": {
+ "name": "extra_section_row_section_id_extra_section_section_id_fk",
+ "tableFrom": "extra_section_row",
+ "tableTo": "extra_section",
+ "schemaTo": "guides",
+ "columnsFrom": [
+ "section_id"
+ ],
+ "columnsTo": [
+ "section_id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "extra_section_row_media_id_media_id_fk": {
+ "name": "extra_section_row_media_id_media_id_fk",
+ "tableFrom": "extra_section_row",
+ "tableTo": "media",
+ "columnsFrom": [
+ "media_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "guides.extra_section": {
+ "name": "extra_section",
+ "schema": "guides",
+ "columns": {
+ "section_id": {
+ "name": "section_id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "extra_section_section_id_guide_section_id_fk": {
+ "name": "extra_section_section_id_guide_section_id_fk",
+ "tableFrom": "extra_section",
+ "tableTo": "guide_section",
+ "schemaTo": "guides",
+ "columnsFrom": [
+ "section_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "guides.glossary_alias": {
+ "name": "glossary_alias",
+ "schema": "guides",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "kind": {
+ "name": "kind",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "catalog_key": {
+ "name": "catalog_key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "alias": {
+ "name": "alias",
+ "type": "varchar(120)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "normalized_alias": {
+ "name": "normalized_alias",
+ "type": "varchar(120)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "glossary_alias_item_normalized_unique": {
+ "name": "glossary_alias_item_normalized_unique",
+ "columns": [
+ {
+ "expression": "kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "catalog_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "normalized_alias",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "glossary_alias_item_idx": {
+ "name": "glossary_alias_item_idx",
+ "columns": [
+ {
+ "expression": "kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "catalog_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "guides.guide_poster_bleeding": {
+ "name": "guide_poster_bleeding",
+ "schema": "guides",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "guide_id": {
+ "name": "guide_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "media_id": {
+ "name": "media_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "amount": {
+ "name": "amount",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 100
+ },
+ "scale": {
+ "name": "scale",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 100
+ },
+ "blend_mode": {
+ "name": "blend_mode",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'multiply'"
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "guide_poster_bleeding_order_idx": {
+ "name": "guide_poster_bleeding_order_idx",
+ "columns": [
+ {
+ "expression": "guide_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sort_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "guide_poster_bleeding_guide_id_guide_id_fk": {
+ "name": "guide_poster_bleeding_guide_id_guide_id_fk",
+ "tableFrom": "guide_poster_bleeding",
+ "tableTo": "guide",
+ "schemaTo": "guides",
+ "columnsFrom": [
+ "guide_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "guide_poster_bleeding_media_id_media_id_fk": {
+ "name": "guide_poster_bleeding_media_id_media_id_fk",
+ "tableFrom": "guide_poster_bleeding",
+ "tableTo": "media",
+ "columnsFrom": [
+ "media_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "guides.guide_section": {
+ "name": "guide_section",
+ "schema": "guides",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "guide_id": {
+ "name": "guide_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar(120)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "note": {
+ "name": "note",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ }
+ },
+ "indexes": {
+ "guide_section_slug_unique": {
+ "name": "guide_section_slug_unique",
+ "columns": [
+ {
+ "expression": "guide_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "slug",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "guide_section_order_idx": {
+ "name": "guide_section_order_idx",
+ "columns": [
+ {
+ "expression": "guide_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sort_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "guide_section_guide_id_guide_id_fk": {
+ "name": "guide_section_guide_id_guide_id_fk",
+ "tableFrom": "guide_section",
+ "tableTo": "guide",
+ "schemaTo": "guides",
+ "columnsFrom": [
+ "guide_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "guides.guide": {
+ "name": "guide",
+ "schema": "guides",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "character_key": {
+ "name": "character_key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar(120)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "overview": {
+ "name": "overview",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "youtube_urls": {
+ "name": "youtube_urls",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "cover_media_id": {
+ "name": "cover_media_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "poster_art_media_id": {
+ "name": "poster_art_media_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "poster_art_offset_x": {
+ "name": "poster_art_offset_x",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "poster_art_offset_y": {
+ "name": "poster_art_offset_y",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "poster_art_scale": {
+ "name": "poster_art_scale",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 100
+ },
+ "poster_art_rotation": {
+ "name": "poster_art_rotation",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "poster_top_colors": {
+ "name": "poster_top_colors",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[\"#000000\",\"#000000\"]'::jsonb"
+ },
+ "poster_bottom_colors": {
+ "name": "poster_bottom_colors",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[\"#000000\",\"#000000\"]'::jsonb"
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "is_public": {
+ "name": "is_public",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "published_at": {
+ "name": "published_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "trashed_at": {
+ "name": "trashed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "guide_character_key_unique": {
+ "name": "guide_character_key_unique",
+ "columns": [
+ {
+ "expression": "character_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "guide_slug_unique": {
+ "name": "guide_slug_unique",
+ "columns": [
+ {
+ "expression": "slug",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "guide_directory_idx": {
+ "name": "guide_directory_idx",
+ "columns": [
+ {
+ "expression": "trashed_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sort_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "guide_cover_media_id_media_id_fk": {
+ "name": "guide_cover_media_id_media_id_fk",
+ "tableFrom": "guide",
+ "tableTo": "media",
+ "columnsFrom": [
+ "cover_media_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ },
+ "guide_poster_art_media_id_media_id_fk": {
+ "name": "guide_poster_art_media_id_media_id_fk",
+ "tableFrom": "guide",
+ "tableTo": "media",
+ "columnsFrom": [
+ "poster_art_media_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.media": {
+ "name": "media",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "object_key": {
+ "name": "object_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_name": {
+ "name": "file_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "mime_type": {
+ "name": "mime_type",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "byte_size": {
+ "name": "byte_size",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "checksum": {
+ "name": "checksum",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "width": {
+ "name": "width",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "height": {
+ "name": "height",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(24)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "current_reference_count": {
+ "name": "current_reference_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_by_id": {
+ "name": "created_by_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "media_object_key_unique": {
+ "name": "media_object_key_unique",
+ "columns": [
+ {
+ "expression": "object_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "media_visibility_idx": {
+ "name": "media_visibility_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "current_reference_count",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "media_created_by_id_user_id_fk": {
+ "name": "media_created_by_id_user_id_fk",
+ "tableFrom": "media",
+ "tableTo": "user",
+ "schemaTo": "auth",
+ "columnsFrom": [
+ "created_by_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.notification": {
+ "name": "notification",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "event_key": {
+ "name": "event_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "body": {
+ "name": "body",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "admin_only": {
+ "name": "admin_only",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "comment_id": {
+ "name": "comment_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "message_id": {
+ "name": "message_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "read_at": {
+ "name": "read_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "notification_user_event_unique": {
+ "name": "notification_user_event_unique",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "event_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "notification_user_time_idx": {
+ "name": "notification_user_time_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "notification_user_unread_idx": {
+ "name": "notification_user_unread_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"notification\".\"read_at\" is null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "notification_comment_idx": {
+ "name": "notification_comment_idx",
+ "columns": [
+ {
+ "expression": "comment_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "notification_message_idx": {
+ "name": "notification_message_idx",
+ "columns": [
+ {
+ "expression": "message_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "notification_user_id_user_id_fk": {
+ "name": "notification_user_id_user_id_fk",
+ "tableFrom": "notification",
+ "tableTo": "user",
+ "schemaTo": "auth",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "notification_comment_id_comment_id_fk": {
+ "name": "notification_comment_id_comment_id_fk",
+ "tableFrom": "notification",
+ "tableTo": "comment",
+ "schemaTo": "comments",
+ "columnsFrom": [
+ "comment_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "notification_message_id_message_id_fk": {
+ "name": "notification_message_id_message_id_fk",
+ "tableFrom": "notification",
+ "tableTo": "message",
+ "schemaTo": "commission",
+ "columnsFrom": [
+ "message_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.outbox_event": {
+ "name": "outbox_event",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "bigint",
+ "primaryKey": true,
+ "notNull": true,
+ "identity": {
+ "type": "always",
+ "name": "outbox_event_id_seq",
+ "schema": "public",
+ "increment": "1",
+ "startWith": "1",
+ "minValue": "1",
+ "maxValue": "9223372036854775807",
+ "cache": "1",
+ "cycle": false
+ }
+ },
+ "topic": {
+ "name": "topic",
+ "type": "varchar(160)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "aggregate_id": {
+ "name": "aggregate_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "event_type": {
+ "name": "event_type",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "payload": {
+ "name": "payload",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(24)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "attempts": {
+ "name": "attempts",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "available_at": {
+ "name": "available_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "processed_at": {
+ "name": "processed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "outbox_pending_idx": {
+ "name": "outbox_pending_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "available_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "auth.session": {
+ "name": "session",
+ "schema": "auth",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ip_address": {
+ "name": "ip_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_agent": {
+ "name": "user_agent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "impersonated_by": {
+ "name": "impersonated_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "session_token_unique": {
+ "name": "session_token_unique",
+ "columns": [
+ {
+ "expression": "token",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "session_user_id_idx": {
+ "name": "session_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "session_user_id_user_id_fk": {
+ "name": "session_user_id_user_id_fk",
+ "tableFrom": "session",
+ "tableTo": "user",
+ "schemaTo": "auth",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "guides.stygian_boss": {
+ "name": "stygian_boss",
+ "schema": "guides",
+ "columns": {
+ "schedule_id": {
+ "name": "schedule_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "level": {
+ "name": "level",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "position": {
+ "name": "position",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "config_id": {
+ "name": "config_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "hp": {
+ "name": "hp",
+ "type": "numeric(18, 2)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "mechanics": {
+ "name": "mechanics",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "recommended_mechanics": {
+ "name": "recommended_mechanics",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "recommended_mechanic_types": {
+ "name": "recommended_mechanic_types",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "image_key": {
+ "name": "image_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "stygian_boss_schedule_idx": {
+ "name": "stygian_boss_schedule_idx",
+ "columns": [
+ {
+ "expression": "schedule_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "level",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "stygian_boss_schedule_id_stygian_schedule_schedule_id_fk": {
+ "name": "stygian_boss_schedule_id_stygian_schedule_schedule_id_fk",
+ "tableFrom": "stygian_boss",
+ "tableTo": "stygian_schedule",
+ "schemaTo": "guides",
+ "columnsFrom": [
+ "schedule_id"
+ ],
+ "columnsTo": [
+ "schedule_id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "stygian_boss_schedule_id_level_position_pk": {
+ "name": "stygian_boss_schedule_id_level_position_pk",
+ "columns": [
+ "schedule_id",
+ "level",
+ "position"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "guides.stygian_recommendation_group": {
+ "name": "stygian_recommendation_group",
+ "schema": "guides",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "schedule_id": {
+ "name": "schedule_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "level": {
+ "name": "level",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "boss_position": {
+ "name": "boss_position",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ }
+ },
+ "indexes": {
+ "stygian_recommendation_group_order_idx": {
+ "name": "stygian_recommendation_group_order_idx",
+ "columns": [
+ {
+ "expression": "schedule_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "level",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "boss_position",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sort_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "stygian_recommendation_group_schedule_id_stygian_schedule_schedule_id_fk": {
+ "name": "stygian_recommendation_group_schedule_id_stygian_schedule_schedule_id_fk",
+ "tableFrom": "stygian_recommendation_group",
+ "tableTo": "stygian_schedule",
+ "schemaTo": "guides",
+ "columnsFrom": [
+ "schedule_id"
+ ],
+ "columnsTo": [
+ "schedule_id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "guides.stygian_recommendation_item": {
+ "name": "stygian_recommendation_item",
+ "schema": "guides",
+ "columns": {
+ "group_id": {
+ "name": "group_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "position": {
+ "name": "position",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "varchar(24)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "catalog_key": {
+ "name": "catalog_key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "weapon_key": {
+ "name": "weapon_key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "artifact_key": {
+ "name": "artifact_key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refinement": {
+ "name": "refinement",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "constellation_level": {
+ "name": "constellation_level",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "element": {
+ "name": "element",
+ "type": "varchar(24)",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "stygian_recommendation_item_group_id_stygian_recommendation_group_id_fk": {
+ "name": "stygian_recommendation_item_group_id_stygian_recommendation_group_id_fk",
+ "tableFrom": "stygian_recommendation_item",
+ "tableTo": "stygian_recommendation_group",
+ "schemaTo": "guides",
+ "columnsFrom": [
+ "group_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "stygian_recommendation_item_group_id_position_pk": {
+ "name": "stygian_recommendation_item_group_id_position_pk",
+ "columns": [
+ "group_id",
+ "position"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "guides.stygian_schedule": {
+ "name": "stygian_schedule",
+ "schema": "guides",
+ "columns": {
+ "schedule_id": {
+ "name": "schedule_id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "challenge_name": {
+ "name": "challenge_name",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "schedule_start_time": {
+ "name": "schedule_start_time",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "schedule_end_time": {
+ "name": "schedule_end_time",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "guides.team_composition": {
+ "name": "team_composition",
+ "schema": "guides",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "guide_id": {
+ "name": "guide_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "condition_id": {
+ "name": "condition_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "rotation_seconds": {
+ "name": "rotation_seconds",
+ "type": "numeric(8, 2)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "combo": {
+ "name": "combo",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ }
+ },
+ "indexes": {
+ "team_composition_order_idx": {
+ "name": "team_composition_order_idx",
+ "columns": [
+ {
+ "expression": "guide_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sort_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "team_composition_guide_id_guide_id_fk": {
+ "name": "team_composition_guide_id_guide_id_fk",
+ "tableFrom": "team_composition",
+ "tableTo": "guide",
+ "schemaTo": "guides",
+ "columnsFrom": [
+ "guide_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "team_composition_condition_id_team_condition_id_fk": {
+ "name": "team_composition_condition_id_team_condition_id_fk",
+ "tableFrom": "team_composition",
+ "tableTo": "team_condition",
+ "schemaTo": "guides",
+ "columnsFrom": [
+ "condition_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "guides.team_condition": {
+ "name": "team_condition",
+ "schema": "guides",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "guide_id": {
+ "name": "guide_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(120)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "note": {
+ "name": "note",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "icon_media_id": {
+ "name": "icon_media_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ }
+ },
+ "indexes": {
+ "team_condition_order_idx": {
+ "name": "team_condition_order_idx",
+ "columns": [
+ {
+ "expression": "guide_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sort_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "team_condition_guide_id_guide_id_fk": {
+ "name": "team_condition_guide_id_guide_id_fk",
+ "tableFrom": "team_condition",
+ "tableTo": "guide",
+ "schemaTo": "guides",
+ "columnsFrom": [
+ "guide_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "team_condition_icon_media_id_media_id_fk": {
+ "name": "team_condition_icon_media_id_media_id_fk",
+ "tableFrom": "team_condition",
+ "tableTo": "media",
+ "columnsFrom": [
+ "icon_media_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "guides.team_member": {
+ "name": "team_member",
+ "schema": "guides",
+ "columns": {
+ "team_id": {
+ "name": "team_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "position": {
+ "name": "position",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "character_key": {
+ "name": "character_key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "weapon_key": {
+ "name": "weapon_key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "artifact_key": {
+ "name": "artifact_key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "second_artifact_key": {
+ "name": "second_artifact_key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "constellation": {
+ "name": "constellation",
+ "type": "varchar(8)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "refinement": {
+ "name": "refinement",
+ "type": "varchar(8)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'1'"
+ },
+ "damage": {
+ "name": "damage",
+ "type": "numeric(18, 2)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "team_member_team_id_team_composition_id_fk": {
+ "name": "team_member_team_id_team_composition_id_fk",
+ "tableFrom": "team_member",
+ "tableTo": "team_composition",
+ "schemaTo": "guides",
+ "columnsFrom": [
+ "team_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "team_member_team_id_position_pk": {
+ "name": "team_member_team_id_position_pk",
+ "columns": [
+ "team_id",
+ "position"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "auth.user": {
+ "name": "user",
+ "schema": "auth",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email_verified": {
+ "name": "email_verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'user'"
+ },
+ "banned": {
+ "name": "banned",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "ban_reason": {
+ "name": "ban_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ban_expires": {
+ "name": "ban_expires",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "user_email_unique": {
+ "name": "user_email_unique",
+ "columns": [
+ {
+ "expression": "email",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "auth.verification": {
+ "name": "verification",
+ "schema": "auth",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "identifier": {
+ "name": "identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "verification_identifier_idx": {
+ "name": "verification_identifier_idx",
+ "columns": [
+ {
+ "expression": "identifier",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "guides.weapon_condition": {
+ "name": "weapon_condition",
+ "schema": "guides",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "guide_id": {
+ "name": "guide_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(120)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "note": {
+ "name": "note",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "icon_media_id": {
+ "name": "icon_media_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ }
+ },
+ "indexes": {
+ "weapon_condition_order_idx": {
+ "name": "weapon_condition_order_idx",
+ "columns": [
+ {
+ "expression": "guide_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sort_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "weapon_condition_guide_id_guide_id_fk": {
+ "name": "weapon_condition_guide_id_guide_id_fk",
+ "tableFrom": "weapon_condition",
+ "tableTo": "guide",
+ "schemaTo": "guides",
+ "columnsFrom": [
+ "guide_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "weapon_condition_icon_media_id_media_id_fk": {
+ "name": "weapon_condition_icon_media_id_media_id_fk",
+ "tableFrom": "weapon_condition",
+ "tableTo": "media",
+ "columnsFrom": [
+ "icon_media_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "guides.weapon_recommendation": {
+ "name": "weapon_recommendation",
+ "schema": "guides",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "condition_id": {
+ "name": "condition_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "weapon_key": {
+ "name": "weapon_key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "refinement": {
+ "name": "refinement",
+ "type": "varchar(8)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "overall_percent": {
+ "name": "overall_percent",
+ "type": "numeric(8, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "personal_percent": {
+ "name": "personal_percent",
+ "type": "numeric(8, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "note": {
+ "name": "note",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ }
+ },
+ "indexes": {
+ "weapon_recommendation_order_idx": {
+ "name": "weapon_recommendation_order_idx",
+ "columns": [
+ {
+ "expression": "condition_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sort_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "weapon_recommendation_condition_id_weapon_condition_id_fk": {
+ "name": "weapon_recommendation_condition_id_weapon_condition_id_fk",
+ "tableFrom": "weapon_recommendation",
+ "tableTo": "weapon_condition",
+ "schemaTo": "guides",
+ "columnsFrom": [
+ "condition_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {},
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
\ No newline at end of file
diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json
index 6a35607..9b48c87 100644
--- a/drizzle/meta/_journal.json
+++ b/drizzle/meta/_journal.json
@@ -561,6 +561,13 @@
"when": 1791400712755,
"tag": "0079_luxuriant_thunderball",
"breakpoints": true
+ },
+ {
+ "idx": 80,
+ "version": "7",
+ "when": 1791566233822,
+ "tag": "0080_wet_colonel_america",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/k8s/base/deployment.yaml b/k8s/base/deployment.yaml
index 986e3e5..7391277 100644
--- a/k8s/base/deployment.yaml
+++ b/k8s/base/deployment.yaml
@@ -127,6 +127,18 @@ spec:
secretKeyRef:
name: buzz-sheet-env
key: SLIP2GO_API_SECRET
+ - name: SLIPOK_BRANCH_ID
+ valueFrom:
+ secretKeyRef:
+ name: buzz-sheet-env
+ key: SLIPOK_BRANCH_ID
+ optional: true
+ - name: SLIPOK_API_KEY
+ valueFrom:
+ secretKeyRef:
+ name: buzz-sheet-env
+ key: SLIPOK_API_KEY
+ optional: true
- name: COMMISSION_DISCORD_WEBHOOK_URL
valueFrom:
secretKeyRef:
diff --git a/lib/commission/payment.test.ts b/lib/commission/payment.test.ts
index 781cf5d..6b90afd 100644
--- a/lib/commission/payment.test.ts
+++ b/lib/commission/payment.test.ts
@@ -1,6 +1,10 @@
import { afterEach, describe, expect, it, vi } from "vitest";
vi.mock("server-only", () => ({}));
+vi.mock("./slip-provider", () => ({
+ getCommissionSlipProvider: vi.fn().mockResolvedValue("slip2go"),
+ slipProviderConfigured: vi.fn().mockReturnValue(true),
+}));
import { promptPayConfig, verifyCommissionSlip } from "./payment";
const names = ["SLIP2GO_VERIFY_URL", "SLIP2GO_API_SECRET", "COMMISSION_PROMPTPAY_TYPE",
diff --git a/lib/commission/payment.ts b/lib/commission/payment.ts
index 184487d..cd128ac 100644
--- a/lib/commission/payment.ts
+++ b/lib/commission/payment.ts
@@ -1,6 +1,7 @@
import "server-only";
import * as z from "zod";
+import { getCommissionSlipProvider, slipProviderConfigured } from "./slip-provider";
import { HttpError } from "@/lib/security/http";
import { normalizePromptPayIdentifier } from "@/lib/commission/promptpay";
@@ -31,7 +32,7 @@ export function promptPayConfig() {
return { identifier, receiverNumber: required("COMMISSION_RECEIVER_ACCOUNT_NUMBER") };
}
-export async function verifyCommissionSlip(file: File, amountBaht: number, checkoutCreatedAt: Date) {
+async function verifySlip2Go(file: File, amountBaht: number) {
const url = new URL(required("SLIP2GO_VERIFY_URL"));
if (url.protocol !== "https:") throw new HttpError(503, "invalid-slip2go-url");
const { identifier, receiverNumber } = promptPayConfig();
@@ -65,10 +66,76 @@ export async function verifyCommissionSlip(file: File, amountBaht: number, check
throw new HttpError(503, `invalid-slip-verification-response:${String(field)}`);
}
const slip = parsedSlip.data;
- if (slip.amount !== amountBaht ||
- Date.parse(slip.dateTime) < checkoutCreatedAt.getTime() - 120000 ||
- Date.parse(slip.dateTime) > Date.now() + 120000)
- throw new HttpError(422, "slip-amount-or-date-mismatch");
- return { referenceId: slip.referenceId, transRef: slip.transRef, amountBaht,
+ return { referenceId: slip.referenceId, transRef: slip.transRef, amountBaht: slip.amount,
transferredAt: new Date(slip.dateTime), providerData: body.data as Record };
}
+
+const slipOKResponse = z.object({
+ success: z.literal(true),
+ data: z.object({
+ success: z.literal(true),
+ transRef: z.string().min(1),
+ amount: z.number().finite(),
+ transTimestamp: z.string().optional(),
+ transDate: z.string().optional(),
+ transTime: z.string().optional(),
+ }).passthrough(),
+}).passthrough();
+
+function slipOKTransferTime(data: z.infer["data"]): Date {
+ let timestamp = data.transTimestamp;
+ if (timestamp === undefined) {
+ if (!/^\d{8}$/.test(data.transDate ?? "") || !/^\d{2}:\d{2}:\d{2}$/.test(data.transTime ?? ""))
+ throw new HttpError(503, "invalid-slip-verification-response:transTimestamp");
+ const date = data.transDate!;
+ timestamp = `${date.slice(0, 4)}-${date.slice(4, 6)}-${date.slice(6, 8)}T${data.transTime}+07:00`;
+ }
+ // Require an explicit timezone and reject dates that JavaScript silently normalizes.
+ const match = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})(?:\.\d+)?(Z|[+-]\d{2}:\d{2})$/.exec(timestamp);
+ const millis = Date.parse(timestamp);
+ if (!match || !Number.isFinite(millis))
+ throw new HttpError(503, "invalid-slip-verification-response:transTimestamp");
+ const dateCheck = new Date(`${match[1]}T${match[2]}Z`);
+ if (!Number.isFinite(dateCheck.getTime()) || dateCheck.toISOString().slice(0, 19) !== `${match[1]}T${match[2]}`)
+ throw new HttpError(503, "invalid-slip-verification-response:transTimestamp");
+ return new Date(millis);
+}
+
+async function verifySlipOK(file: File, amountBaht: number) {
+ const form = new FormData();
+ form.set("files", file);
+ form.set("log", "true");
+ form.set("amount", String(amountBaht));
+ let response: Response;
+ try {
+ response = await fetch(`https://api.slipok.com/api/line/apikey/${required("SLIPOK_BRANCH_ID")}`, {
+ method: "POST", headers: { "x-authorization": required("SLIPOK_API_KEY") },
+ body: form, signal: AbortSignal.timeout(15000), cache: "no-store",
+ });
+ } catch { throw new HttpError(503, "slip-verification-unavailable"); }
+ const body: unknown = await response.json().catch(() => null);
+ const error = z.object({ code: z.number().int() }).safeParse(body);
+ if (error.success) {
+ const code = error.data.code;
+ const unavailable = response.status >= 500 || response.status === 401 || response.status === 429 ||
+ [1000, 1001, 1002, 1003, 1004, 1009, 1010, 1015].includes(code);
+ throw new HttpError(unavailable ? 503 : 422, `slipok-${unavailable ? "service" : "rejected"}:${code}`);
+ }
+ if (!response.ok) throw new HttpError(503, "slip-verification-unavailable");
+ const parsed = slipOKResponse.safeParse(body);
+ if (!parsed.success) throw new HttpError(503, "invalid-slip-verification-response");
+ const slip = parsed.data.data;
+ return { referenceId: `slipok:${slip.transRef}`, transRef: slip.transRef, amountBaht: slip.amount,
+ transferredAt: slipOKTransferTime(slip), providerData: parsed.data as Record };
+}
+
+export async function verifyCommissionSlip(file: File, amountBaht: number, checkoutCreatedAt: Date) {
+ const provider = await getCommissionSlipProvider();
+ if (!slipProviderConfigured(provider)) throw new HttpError(503, "slip-provider-not-configured");
+ const slip = provider === "slipok" ? await verifySlipOK(file, amountBaht) : await verifySlip2Go(file, amountBaht);
+ if (slip.amountBaht !== amountBaht ||
+ slip.transferredAt.getTime() < checkoutCreatedAt.getTime() - 120000 ||
+ slip.transferredAt.getTime() > Date.now() + 120000)
+ throw new HttpError(422, "slip-amount-or-date-mismatch");
+ return { ...slip, provider };
+}
diff --git a/lib/commission/slip-provider.test.ts b/lib/commission/slip-provider.test.ts
new file mode 100644
index 0000000..61ea975
--- /dev/null
+++ b/lib/commission/slip-provider.test.ts
@@ -0,0 +1,31 @@
+import { afterEach, describe, expect, it, vi } from "vitest";
+
+const { limit } = vi.hoisted(() => ({ limit: vi.fn() }));
+vi.mock("server-only", () => ({}));
+vi.mock("@/db", () => ({ getDb: () => ({ select: () => ({ from: () => ({ where: () => ({ limit }) }) }) }) }));
+import { getCommissionSlipProvider, slipProviderConfigured } from "./slip-provider";
+afterEach(() => { vi.unstubAllEnvs(); vi.clearAllMocks(); });
+
+describe("commission provider configuration", () => {
+ it("reads fresh settings after switching", async () => {
+ limit.mockResolvedValueOnce([{ slipProvider: "slip2go" }]).mockResolvedValueOnce([{ slipProvider: "slipok" }]);
+ expect(await getCommissionSlipProvider()).toBe("slip2go");
+ expect(await getCommissionSlipProvider()).toBe("slipok");
+ expect(limit).toHaveBeenCalledTimes(2);
+ });
+ it.each([{ rows: [] }, { rows: [{ slipProvider: "invalid" }] }])("rejects missing or invalid settings", async ({ rows }) => {
+ limit.mockResolvedValueOnce(rows);
+ await expect(getCommissionSlipProvider()).rejects.toMatchObject({ status: 503 });
+ });
+ it("requires an HTTPS Slip2Go endpoint and key", () => {
+ vi.stubEnv("SLIP2GO_API_SECRET", "secret");
+ vi.stubEnv("SLIP2GO_VERIFY_URL", "http://slip.test");
+ expect(slipProviderConfigured("slip2go")).toBe(false);
+ vi.stubEnv("SLIP2GO_VERIFY_URL", "invalid");
+ expect(slipProviderConfigured("slip2go")).toBe(false);
+ vi.stubEnv("SLIP2GO_VERIFY_URL", "https://slip.test");
+ expect(slipProviderConfigured("slip2go")).toBe(true);
+ vi.stubEnv("SLIP2GO_API_SECRET", "");
+ expect(slipProviderConfigured("slip2go")).toBe(false);
+ });
+});
diff --git a/lib/commission/slip-provider.ts b/lib/commission/slip-provider.ts
new file mode 100644
index 0000000..7d7b595
--- /dev/null
+++ b/lib/commission/slip-provider.ts
@@ -0,0 +1,29 @@
+import "server-only";
+
+import { eq } from "drizzle-orm";
+import { getDb } from "@/db";
+import { commissionSettings } from "@/db/schema";
+import { HttpError } from "@/lib/security/http";
+
+export type SlipProvider = "slip2go" | "slipok";
+
+export function isSlipProvider(value: unknown): value is SlipProvider {
+ return value === "slip2go" || value === "slipok";
+}
+
+export function slipProviderConfigured(provider: SlipProvider): boolean {
+ if (provider === "slipok")
+ return /^\d+$/.test(process.env.SLIPOK_BRANCH_ID ?? "") && Boolean(process.env.SLIPOK_API_KEY?.trim());
+ try {
+ return new URL(process.env.SLIP2GO_VERIFY_URL ?? "").protocol === "https:" &&
+ Boolean(process.env.SLIP2GO_API_SECRET?.trim());
+ } catch { return false; }
+}
+
+export async function getCommissionSlipProvider(): Promise {
+ const [settings] = await getDb().select({ slipProvider: commissionSettings.slipProvider })
+ .from(commissionSettings).where(eq(commissionSettings.id, 1)).limit(1);
+ if (!settings || !isSlipProvider(settings.slipProvider))
+ throw new HttpError(503, "slip-provider-not-configured");
+ return settings.slipProvider;
+}
diff --git a/lib/commission/slip-upload-payment.test.ts b/lib/commission/slip-upload-payment.test.ts
new file mode 100644
index 0000000..8bd19f4
--- /dev/null
+++ b/lib/commission/slip-upload-payment.test.ts
@@ -0,0 +1,54 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+const mocks = vi.hoisted(() => ({
+ limit: vi.fn(), verifyCommissionSlip: vi.fn(), write: vi.fn(), delete: vi.fn(),
+ paymentValues: vi.fn(), ticketReturning: vi.fn(), notify: vi.fn(), discord: vi.fn(),
+}));
+vi.mock("server-only", () => ({}));
+vi.mock("@/lib/redis/client", () => ({ getRedisClient: async () => ({ set: async () => "OK", eval: async () => 0 }) }));
+vi.mock("@/lib/media/inspect", () => ({ inspectImage: vi.fn() }));
+vi.mock("@/lib/media/storage", () => ({ getMediaStorage: async () => ({ write: mocks.write, delete: mocks.delete }) }));
+vi.mock("@/lib/commission/payment", () => ({ verifyCommissionSlip: mocks.verifyCommissionSlip }));
+vi.mock("@/lib/commission/server", () => ({ notifyCommission: mocks.notify }));
+vi.mock("@/lib/commission/discord", () => ({ notifyPaidTicketDiscord: mocks.discord }));
+vi.mock("@/db", () => ({ getDb: () => ({
+ select: () => ({ from: () => ({ where: () => ({ limit: mocks.limit }) }) }),
+ transaction: async (fn: (tx: unknown) => Promise) => {
+ let inserts = 0;
+ return fn({ insert: () => ++inserts === 1
+ ? { values: () => ({ returning: mocks.ticketReturning }) }
+ : { values: mocks.paymentValues } });
+ },
+}) }));
+import { verifyAndCreateTicket } from "./slip-upload";
+import type { Checkout } from "./slip-upload";
+
+const checkout = { id: "checkout", userId: "customer", amountBaht: 100, createdAt: new Date() } as Checkout;
+const file = new File(["image"], "slip.png", { type: "image/png" });
+beforeEach(() => {
+ vi.clearAllMocks();
+ mocks.limit.mockResolvedValue([]);
+ mocks.ticketReturning.mockResolvedValue([{ id: "ticket" }]);
+ mocks.paymentValues.mockResolvedValue(undefined);
+ mocks.write.mockResolvedValue(undefined);
+ mocks.delete.mockResolvedValue(undefined);
+});
+
+describe("verified commission payment persistence", () => {
+ it.each(["slip2go", "slipok"])("persists the %s provider without changing the bank transaction key", async (provider) => {
+ mocks.verifyCommissionSlip.mockResolvedValue({ provider, transRef: "bank-ref", referenceId: `${provider}:ref`,
+ amountBaht: 100, transferredAt: new Date(), providerData: { success: true } });
+ expect(await verifyAndCreateTicket(checkout, file)).toEqual({ ticketId: "ticket", created: true });
+ expect(mocks.paymentValues).toHaveBeenCalledWith(expect.objectContaining({ provider, transRef: "bank-ref",
+ checkoutId: "checkout", ticketId: "ticket" }));
+ });
+ it("rejects a bank transaction already paid through another provider and cleans up the upload", async () => {
+ mocks.verifyCommissionSlip.mockResolvedValue({ provider: "slipok", transRef: "bank-ref",
+ referenceId: "slipok:bank-ref", amountBaht: 100, transferredAt: new Date(), providerData: {} });
+ mocks.paymentValues.mockRejectedValueOnce(Object.assign(new Error("duplicate bank reference"), { code: "23505" }));
+ await expect(verifyAndCreateTicket(checkout, file)).rejects.toMatchObject({ status: 409, message: "payment-already-used" });
+ expect(mocks.delete).toHaveBeenCalledOnce();
+ expect(mocks.notify).not.toHaveBeenCalled();
+ expect(mocks.discord).not.toHaveBeenCalled();
+ });
+});
diff --git a/lib/commission/slipok.test.ts b/lib/commission/slipok.test.ts
new file mode 100644
index 0000000..568bd10
--- /dev/null
+++ b/lib/commission/slipok.test.ts
@@ -0,0 +1,119 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+vi.mock("server-only", () => ({}));
+vi.mock("./slip-provider", () => ({
+ getCommissionSlipProvider: vi.fn(), slipProviderConfigured: vi.fn().mockReturnValue(true),
+}));
+import { getCommissionSlipProvider, slipProviderConfigured } from "./slip-provider";
+import { verifyCommissionSlip } from "./payment";
+import { verificationError } from "./verification-error";
+
+const checkoutDate = new Date("2026-10-10T10:00:00Z");
+const validData = { success: true, data: { success: true, transRef: "bank-ref", amount: 100,
+ transTimestamp: "2026-10-10T10:01:00.000Z" } };
+const file = new File(["image"], "slip.png", { type: "image/png" });
+const fetchMock = vi.fn();
+const verify = () => verifyCommissionSlip(file, 100, checkoutDate);
+
+beforeEach(() => {
+ vi.clearAllMocks();
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date("2026-10-10T10:02:00Z"));
+ vi.stubEnv("SLIPOK_BRANCH_ID", "123");
+ vi.stubEnv("SLIPOK_API_KEY", "secret");
+ vi.mocked(getCommissionSlipProvider).mockResolvedValue("slipok");
+ vi.mocked(slipProviderConfigured).mockReturnValue(true);
+ fetchMock.mockResolvedValue(Response.json(validData));
+ vi.stubGlobal("fetch", fetchMock);
+});
+afterEach(() => { vi.useRealTimers(); vi.unstubAllGlobals(); vi.unstubAllEnvs(); });
+
+describe("SlipOK verification", () => {
+ it("sends the image, receiver logging and expected amount and normalizes the transfer", async () => {
+ const result = await verify();
+ expect(result).toMatchObject({ provider: "slipok", referenceId: "slipok:bank-ref",
+ transRef: "bank-ref", amountBaht: 100, transferredAt: new Date("2026-10-10T10:01:00Z"),
+ providerData: validData });
+ const [url, init] = fetchMock.mock.calls[0];
+ expect(url).toBe("https://api.slipok.com/api/line/apikey/123");
+ expect(init).toMatchObject({ method: "POST", headers: { "x-authorization": "secret" }, cache: "no-store" });
+ expect(init.body.get("files")).toBeInstanceOf(File);
+ expect(init.body.get("log")).toBe("true");
+ expect(init.body.get("amount")).toBe("100");
+ expect(init.body.get("file")).toBeNull();
+ });
+
+ it("interprets legacy date and time as Bangkok time", async () => {
+ fetchMock.mockResolvedValue(Response.json({ success: true, data: { success: true,
+ transRef: "bank-ref", amount: 100, transDate: "20261010", transTime: "17:01:00" } }));
+ expect((await verify()).transferredAt).toEqual(new Date("2026-10-10T10:01:00Z"));
+ });
+
+ it.each(["invalid", "2026-02-30T10:01:00Z", "2026-10-10T25:01:00Z", "2026-10-10T10:01:00"])
+ ("rejects an invalid timestamp %s", async (transTimestamp) => {
+ fetchMock.mockResolvedValue(Response.json({ ...validData, data: { ...validData.data, transTimestamp } }));
+ await expect(verify()).rejects.toMatchObject({ status: 503 });
+ });
+
+ it.each([{}, { success: true, data: {} }, { ...validData, success: false },
+ { ...validData, data: { ...validData.data, success: false } }])("rejects malformed or unverified responses", async (body) => {
+ fetchMock.mockResolvedValue(Response.json(body));
+ await expect(verify()).rejects.toMatchObject({ status: 503 });
+ });
+
+ it.each([1000, 1001, 1002, 1003, 1004, 1009, 1010, 1015])("classifies service error %s", async (code) => {
+ fetchMock.mockResolvedValue(Response.json({ code }, { status: 400 }));
+ await expect(verify()).rejects.toMatchObject({ status: 503, message: `slipok-service:${code}` });
+ });
+ it.each([1005, 1006, 1007, 1008, 1011, 1012, 1013, 1014])("rejects slip error %s, including accompanying data", async (code) => {
+ fetchMock.mockResolvedValue(Response.json({ code, data: validData.data }, { status: 400 }));
+ await expect(verify()).rejects.toMatchObject({ status: 422, message: `slipok-rejected:${code}` });
+ expect(fetchMock).toHaveBeenCalledTimes(1);
+ });
+
+ it.each([401, 429, 500])("handles HTTP %s without valid JSON", async (status) => {
+ fetchMock.mockResolvedValue(new Response("upstream failure", { status }));
+ await expect(verify()).rejects.toMatchObject({ status: 503 });
+ });
+
+ it("does not fall back on timeout", async () => {
+ fetchMock.mockRejectedValue(new DOMException("Timed out", "TimeoutError"));
+ await expect(verify()).rejects.toMatchObject({ status: 503, message: "slip-verification-unavailable" });
+ expect(fetchMock).toHaveBeenCalledTimes(1);
+ });
+
+ it.each([{ amount: 80 }, { transTimestamp: "2026-10-10T09:57:59Z" },
+ { transTimestamp: "2026-10-10T10:04:01Z" }])("rejects mismatched amount or transfer time", async (fields) => {
+ fetchMock.mockResolvedValue(Response.json({ ...validData, data: { ...validData.data, ...fields } }));
+ await expect(verify()).rejects.toMatchObject({ status: 422, message: "slip-amount-or-date-mismatch" });
+ });
+ it.each(["2026-10-10T09:58:00Z", "2026-10-10T10:04:00Z"])("accepts the date boundary %s", async (transTimestamp) => {
+ fetchMock.mockResolvedValue(Response.json({ ...validData, data: { ...validData.data, transTimestamp } }));
+ await expect(verify()).resolves.toMatchObject({ transferredAt: new Date(transTimestamp) });
+ });
+
+ it("reads the provider once on every attempt and keeps bank references across a switch", async () => {
+ expect((await verify()).transRef).toBe("bank-ref");
+ vi.mocked(getCommissionSlipProvider).mockResolvedValue("slip2go");
+ vi.stubEnv("SLIP2GO_VERIFY_URL", "https://slip2go.test/verify");
+ vi.stubEnv("SLIP2GO_API_SECRET", "secret");
+ vi.stubEnv("COMMISSION_PROMPTPAY_TYPE", "mobile");
+ vi.stubEnv("COMMISSION_PROMPTPAY_VALUE", "0812345678");
+ vi.stubEnv("COMMISSION_RECEIVER_ACCOUNT_NUMBER", "0812345678");
+ fetchMock.mockResolvedValue(Response.json({ code: "200200", data: { referenceId: "provider-ref",
+ transRef: "bank-ref", amount: 100, dateTime: "2026-10-10T10:01:00Z" } }));
+ expect(await verify()).toMatchObject({ provider: "slip2go", transRef: "bank-ref", referenceId: "provider-ref" });
+ expect(getCommissionSlipProvider).toHaveBeenCalledTimes(2);
+ });
+
+ it("does not call an unconfigured provider", async () => {
+ vi.mocked(slipProviderConfigured).mockReturnValue(false);
+ await expect(verify()).rejects.toMatchObject({ status: 503, message: "slip-provider-not-configured" });
+ expect(fetchMock).not.toHaveBeenCalled();
+ });
+ it("warns against paying again after a provider duplicate rejection", () => {
+ expect(verificationError("slipok-rejected:1012")).toContain("ก่อนโอนซ้ำ");
+ expect(verificationError("slipok-service:1002")).toContain("API Key");
+ expect(verificationError("slipok-service:1010")).toContain("รอสักครู่");
+ });
+});
diff --git a/lib/commission/verification-error.ts b/lib/commission/verification-error.ts
index 5a770f4..daf1624 100644
--- a/lib/commission/verification-error.ts
+++ b/lib/commission/verification-error.ts
@@ -1,4 +1,26 @@
export function verificationError(code: string | undefined): string {
+ if (code?.startsWith("slipok-service:") || code?.startsWith("slipok-rejected:")) {
+ const reason = code.slice(code.indexOf(":") + 1);
+ switch (reason) {
+ case "1000": return "ตั้งค่าคำขอตรวจสลิป SlipOK ไม่ถูกต้อง กรุณาแจ้งผู้ดูแลระบบ (1000)";
+ case "1001": return "ไม่พบสาขา SlipOK ที่ตั้งไว้ กรุณาแจ้งผู้ดูแลระบบ (1001)";
+ case "1002": return "SlipOK ไม่ยอมรับ API Key ที่ตั้งไว้ กรุณาแจ้งผู้ดูแลระบบ (1002)";
+ case "1003": return "แพ็กเกจ SlipOK หมดอายุ กรุณาแจ้งผู้ดูแลระบบ (1003)";
+ case "1004": return "โควต้า SlipOK ไม่เพียงพอ กรุณาแจ้งผู้ดูแลระบบ (1004)";
+ case "1005":
+ case "1006": return `รูปสลิปไม่ถูกต้อง กรุณาอัปโหลดรูปจากแอปธนาคาร (${reason})`;
+ case "1007":
+ case "1008": return `อ่าน QR สำหรับตรวจสอบการชำระเงินไม่ได้ กรุณาอัปโหลดรูปสลิปจากแอปธนาคาร (${reason})`;
+ case "1009": return "ธนาคารขัดข้องชั่วคราว กรุณาตรวจสลิปอีกครั้งใน 15 นาที (1009)";
+ case "1010": return "ธนาคารยังไม่พร้อมยืนยันสลิป กรุณารอสักครู่แล้วลองอีกครั้ง (1010)";
+ case "1011": return "QR หมดอายุหรือไม่พบรายการโอน กรุณาตรวจสอบสลิป (1011)";
+ case "1012": return "สลิปนี้เคยถูกตรวจสอบแล้ว กรุณาติดต่อผู้ดูแลก่อนโอนซ้ำ (1012)";
+ case "1013": return "ยอดเงินในสลิปไม่ตรงกับยอดคำขอ (1013)";
+ case "1014": return "บัญชีผู้รับในสลิปไม่ตรงกับบัญชีที่ตั้งไว้ใน SlipOK (1014)";
+ case "1015": return "ไม่พบแพ็กเกจ SlipOK กรุณาแจ้งผู้ดูแลระบบ (1015)";
+ default: return "SlipOK ตรวจสอบสลิปไม่สำเร็จ กรุณาลองอีกครั้งหรือติดต่อผู้ดูแลระบบ";
+ }
+ }
const reason = code?.startsWith("slip-rejected:") ? code.slice("slip-rejected:".length)
: code?.startsWith("slip2go-service:") ? code.slice("slip2go-service:".length) : code;
switch (reason) {
@@ -15,6 +37,7 @@ export function verificationError(code: string | undefined): string {
case "400400": return "ตั้งค่าเงื่อนไขตรวจสอบสลิปไม่ถูกต้อง กรุณาแจ้งผู้ดูแลระบบ (400400)";
case "401001": return "Slip2Go ไม่ยอมรับ API Secret ที่ตั้งไว้ กรุณาแจ้งผู้ดูแลระบบ (401001)";
case "401005": return "โทเคน Slip2Go หมด กรุณาแจ้งผู้ดูแลระบบ (401005)";
+ case "slip-provider-not-configured": return "ยังไม่ได้ตั้งค่าผู้ให้บริการตรวจสลิป กรุณาแจ้งผู้ดูแลระบบ";
case "slip-amount-or-date-mismatch": return "ยอดเงินหรือวันเวลาในสลิปไม่ตรงกับคำขอนี้ กรุณาใช้สลิปจากการชำระเงินครั้งนี้";
case "payment-already-used": return "สลิปนี้ถูกใช้กับคำขออื่นแล้ว";
case "slip-verification-in-progress": return "กำลังตรวจสอบสลิปอยู่ กรุณารอสักครู่";