Files
buzz-sheet/lib/commission/slipok.test.ts
T
gunshiz 7ba513c2bb
CI / Verify (push) Successful in 1m56s
CI / Build immutable images and deploy (push) Successful in 3m26s
feat : add slipok and switcher to switch between slip2go and slipok
2026-10-10 00:31:12 +07:00

120 lines
6.5 KiB
TypeScript

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("รอสักครู่");
});
});