Files
buzz/app/(ui)/donate/api.ts
T

120 lines
3.6 KiB
TypeScript

"use server";
import { sql } from "drizzle-orm";
import { db } from "@/lib/db";
import { donations, endgameSlips, submissions } from "@/lib/db/schema";
import { sse } from "@/lib/db/sse-endpoints";
import { checkSlip } from "@/lib/payment";
import { getPostHogClient } from "@/lib/posthog-server";
import { generatePromptPayPayload } from "@/lib/promptpay";
import { fileToDataUrl } from "@/lib/utils";
import {
createPromptPayQrGenerator,
parseDonationForm,
processDonation,
} from "./service";
const {
PROMPTPAY_IDENTIFIER,
PROMPTPAY_TYPE,
TMN_DEST_PHONE_NUM,
SASTIFY_API_PRIVKEY,
} = process.env;
if (!PROMPTPAY_IDENTIFIER || !PROMPTPAY_TYPE)
console.error("Warning: Missing PromptPay configuration!");
const generateQr = createPromptPayQrGenerator({
identifier: PROMPTPAY_IDENTIFIER,
type: PROMPTPAY_TYPE,
generate: generatePromptPayPayload,
});
export async function generateQrcode(amount: number | string) {
return generateQr(amount);
}
export async function submitDonation(data: FormData) {
const parsed = parseDonationForm(data);
if (!parsed.success) return parsed.result;
const ph = getPostHogClient();
return db.transaction(async (tx) =>
processDonation(parsed.data, {
createId: crypto.randomUUID,
checkPromptPay: checkSlip,
async savePromptPaySlip({ slip, amount, result }) {
const [saved] = await tx
.insert(endgameSlips)
.values({
slip,
amount: amount.toString(),
data: result as typeof endgameSlips.$inferInsert.data,
ref: result.data.transRef,
})
.returning({ id: endgameSlips.id })
.catch((error) => {
console.log(error);
return [];
});
return !!saved;
},
async redeemTrueMoney({ amount, link }) {
return fetch("https://api.sastify.xyz/v1/gateway/tmn", {
method: "POST",
headers: {
authorization: `Bearer ${SASTIFY_API_PRIVKEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
amount,
phone_number: TMN_DEST_PHONE_NUM,
voucher_url: link,
}),
signal: AbortSignal.timeout(30_000),
})
.then((response) => response.json())
.catch((error) => ({
success: false as const,
message: error instanceof Error ? error.message : String(error),
}));
},
async downscaleImage(image) {
return new Bun.Image(await image.arrayBuffer())
.resize(512, 512)
.webp()
.toBuffer();
},
async saveDonation(input) {
const [{ id }] = await tx
.insert(donations)
.values(input)
.returning({ id: donations.id });
return id;
},
async promoteArtifact({ name, message, uid }) {
await tx.transaction(async (artifactTx) => {
await artifactTx
.insert(submissions)
.values({
name,
comment: message,
uid,
queue: null as unknown as undefined,
})
.onConflictDoUpdate({
target: submissions.uid,
set: {
comment: sql`${submissions.comment} || ${"\n"}::text || ${message}::text`,
promoted: true,
},
});
});
},
imageToDataUrl: fileToDataUrl,
publishPopup: (popup) => sse.donate.pub("ping", popup),
publishUpdate: () => sse.donate.pub("update", null),
capture: (event) => ph.capture(event),
}),
);
}