Files
buzz-sheet/lib/commission/payment.ts
T
gunshiz 0022625edb
CI / Verify (push) Successful in 3m14s
CI / Build immutable images and deploy (push) Successful in 4m0s
feat : big update
2026-10-01 18:37:15 +07:00

76 lines
3.2 KiB
TypeScript

import "server-only";
import * as z from "zod";
import { HttpError } from "@/lib/security/http";
import { normalizePromptPayIdentifier } from "@/lib/commission/promptpay";
const slipResponse = z.object({ code: z.string() }).passthrough();
const verifiedSlip = z.object({
referenceId: z.string().min(1),
transRef: z.string().min(1),
amount: z.number(),
dateTime: z.string().refine((value) => !Number.isNaN(Date.parse(value))),
}).passthrough();
function required(name: string) {
const value = process.env[name];
if (!value) throw new HttpError(503, `${name}-not-configured`);
return value;
}
export function promptPayConfig() {
const type = required("COMMISSION_PROMPTPAY_TYPE");
if (type !== "mobile" && type !== "nationalId" && type !== "ewallet")
throw new HttpError(503, "invalid-promptpay-type");
let identifier;
try {
identifier = normalizePromptPayIdentifier({ type, value: required("COMMISSION_PROMPTPAY_VALUE") });
} catch {
throw new HttpError(503, "invalid-promptpay-value");
}
return { identifier, receiverNumber: required("COMMISSION_RECEIVER_ACCOUNT_NUMBER") };
}
export async function verifyCommissionSlip(file: File, amountBaht: number, checkoutCreatedAt: Date) {
const url = new URL(required("SLIP2GO_VERIFY_URL"));
if (url.protocol !== "https:") throw new HttpError(503, "invalid-slip2go-url");
const { identifier, receiverNumber } = promptPayConfig();
const form = new FormData();
form.set("file", file);
form.set("payload", JSON.stringify({
checkDuplicate: true,
checkReceiver: [{
accountType: { mobile: "02001", nationalId: "02003", ewallet: "02004" }[identifier.type],
accountNumber: receiverNumber,
}],
checkAmount: { type: "eq", amount: amountBaht.toFixed(2) },
}));
let response: Response;
try {
response = await fetch(url, { method: "POST", headers: { Authorization: `Bearer ${required("SLIP2GO_API_SECRET")}` },
body: form, signal: AbortSignal.timeout(15000), cache: "no-store" });
} catch {
throw new HttpError(503, "slip-verification-unavailable");
}
const body = slipResponse.safeParse(await response.json().catch(() => null));
if (!body.success) throw new HttpError(503, "invalid-slip-verification-response");
const code = body.data.code;
if (response.status >= 500 || response.status === 401 || code.startsWith("401") ||
code === "400400" || code === "200502")
throw new HttpError(503, `slip2go-service:${code}`);
if (!response.ok || code !== "200200")
throw new HttpError(422, `slip-rejected:${code}`);
const parsedSlip = verifiedSlip.safeParse(body.data.data);
if (!parsedSlip.success) {
const field = parsedSlip.error.issues[0]?.path[0] ?? "data";
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,
transferredAt: new Date(slip.dateTime), providerData: body.data as Record<string, unknown> };
}