feat : add slipok and switcher to switch between slip2go and slipok
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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<string, unknown> };
|
||||
}
|
||||
|
||||
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<typeof slipOKResponse>["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<string, unknown> };
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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<SlipProvider> {
|
||||
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;
|
||||
}
|
||||
@@ -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<unknown>) => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -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("รอสักครู่");
|
||||
});
|
||||
});
|
||||
@@ -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 "กำลังตรวจสอบสลิปอยู่ กรุณารอสักครู่";
|
||||
|
||||
Reference in New Issue
Block a user