From 7ba513c2bb92a2c9f6a05a6cee372ce19a3e1688 Mon Sep 17 00:00:00 2001 From: gunshiz Date: Sat, 10 Oct 2026 00:31:12 +0700 Subject: [PATCH] feat : add slipok and switcher to switch between slip2go and slipok --- .env.example | 3 + app/admin/commission/actions.test.ts | 2 + app/admin/commission/actions.ts | 19 + app/admin/commission/page.tsx | 12 +- .../commission/slip-provider-actions.test.ts | 58 + .../admin-slip-provider-control.tsx | 69 + db/schema.ts | 5 +- drizzle/0080_wet_colonel_america.sql | 4 + drizzle/meta/0080_snapshot.json | 5202 +++++++++++++++++ drizzle/meta/_journal.json | 7 + k8s/base/deployment.yaml | 12 + lib/commission/payment.test.ts | 4 + lib/commission/payment.ts | 79 +- lib/commission/slip-provider.test.ts | 31 + lib/commission/slip-provider.ts | 29 + lib/commission/slip-upload-payment.test.ts | 54 + lib/commission/slipok.test.ts | 119 + lib/commission/verification-error.ts | 23 + 18 files changed, 5722 insertions(+), 10 deletions(-) create mode 100644 app/admin/commission/slip-provider-actions.test.ts create mode 100644 components/commission/admin-slip-provider-control.tsx create mode 100644 drizzle/0080_wet_colonel_america.sql create mode 100644 drizzle/meta/0080_snapshot.json create mode 100644 lib/commission/slip-provider.test.ts create mode 100644 lib/commission/slip-provider.ts create mode 100644 lib/commission/slip-upload-payment.test.ts create mode 100644 lib/commission/slipok.test.ts 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 { + if (pending) return; + if (nextOpen) { setSelected(provider); setError(""); } + setOpen(nextOpen); + }}> + }> + + + + ตั้งค่าการชำระเงิน + + + ผู้ให้บริการ + { + const value = values[0]; + if (value === "slip2go" || value === "slipok") { setSelected(value); setError(""); } + }}> + Slip2Go + SlipOK + + + {error && {error}} + + + + + + + ; +} 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 "กำลังตรวจสอบสลิปอยู่ กรุณารอสักครู่";