pelagia-portal/App/app/(portal)/admin/vendors/actions.ts
Hardik d689ef8893 fix(vendors): fix transaction timeout and misleading error on vendor delete
Increase the Prisma interactive transaction timeout from the default 5s
to 30s so that the four sequential nullification + delete queries complete
reliably on a seeded database (P2028 timeout was the root cause).

Wrap the transaction in a try/catch so that if a timeout does still occur
the user sees "Delete timed out — please try again." instead of an
unhandled 500 that previously manifested as the misleading "referenced in
submitted or active purchase orders" error message.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 23:34:35 +05:30

196 lines
6.6 KiB
TypeScript

"use server";
import { auth } from "@/auth";
import { db } from "@/lib/db";
import { hasPermission } from "@/lib/permissions";
import { geocodePincode } from "@/lib/geo";
import { z } from "zod";
import { revalidatePath } from "next/cache";
type ActionResult = { ok: true } | { error: string };
const contactSchema = z.object({
name: z.string().min(1),
role: z.string().optional(),
mobile: z.string().optional(),
email: z.string().optional(),
isPrimary: z.boolean().default(false),
});
const vendorSchema = z.object({
name: z.string().min(1, "Vendor name is required"),
vendorId: z.string().optional(),
address: z.string().optional(),
pincode: z.string().optional(),
gstin: z.string().optional(),
});
function parseContacts(formData: FormData) {
const contacts: z.infer<typeof contactSchema>[] = [];
let i = 0;
while (formData.has(`contacts[${i}].name`)) {
const name = (formData.get(`contacts[${i}].name`) as string).trim();
if (name) {
contacts.push({
name,
role: (formData.get(`contacts[${i}].role`) as string) || undefined,
mobile: (formData.get(`contacts[${i}].mobile`) as string) || undefined,
email: (formData.get(`contacts[${i}].email`) as string) || undefined,
isPrimary: formData.get(`contacts[${i}].isPrimary`) === "true",
});
}
i++;
}
return contacts;
}
async function resolveLatLng(pincode?: string) {
if (!pincode) return { latitude: null, longitude: null };
const coords = await geocodePincode(pincode);
return { latitude: coords?.lat ?? null, longitude: coords?.lng ?? null };
}
export async function createVendor(formData: FormData): Promise<ActionResult> {
const session = await auth();
if (!session?.user || !hasPermission(session.user.role, "manage_vendors")) {
return { error: "Unauthorized" };
}
const parsed = vendorSchema.safeParse({
name: formData.get("name"),
vendorId: formData.get("vendorId") || undefined,
address: formData.get("address") || undefined,
pincode: formData.get("pincode") || undefined,
gstin: formData.get("gstin") || undefined,
});
if (!parsed.success) return { error: parsed.error.errors[0]?.message ?? "Validation failed" };
const data = parsed.data;
if (data.vendorId) {
const exists = await db.vendor.findUnique({ where: { vendorId: data.vendorId } });
if (exists) return { error: "A vendor with that Vendor ID already exists" };
}
const { latitude, longitude } = await resolveLatLng(data.pincode);
const contacts = parseContacts(formData);
await db.vendor.create({
data: {
name: data.name,
vendorId: data.vendorId ?? null,
address: data.address ?? null,
pincode: data.pincode ?? null,
gstin: data.gstin ?? null,
latitude,
longitude,
isVerified: !!data.vendorId,
contacts: contacts.length > 0 ? { create: contacts } : undefined,
},
});
revalidatePath("/admin/vendors");
return { ok: true };
}
export async function updateVendor(formData: FormData): Promise<ActionResult> {
const session = await auth();
if (!session?.user || !hasPermission(session.user.role, "manage_vendors")) {
return { error: "Unauthorized" };
}
const id = formData.get("id") as string;
if (!id) return { error: "Vendor ID is required" };
const parsed = vendorSchema.safeParse({
name: formData.get("name"),
vendorId: formData.get("vendorId") || undefined,
address: formData.get("address") || undefined,
pincode: formData.get("pincode") || undefined,
gstin: formData.get("gstin") || undefined,
});
if (!parsed.success) return { error: parsed.error.errors[0]?.message ?? "Validation failed" };
const data = parsed.data;
if (data.vendorId) {
const conflict = await db.vendor.findFirst({ where: { vendorId: data.vendorId, id: { not: id } } });
if (conflict) return { error: "Another vendor already has that Vendor ID" };
}
const existing = await db.vendor.findUnique({ where: { id }, select: { pincode: true, latitude: true, longitude: true } });
const pincodeChanged = data.pincode !== (existing?.pincode ?? undefined);
const { latitude, longitude } = pincodeChanged
? await resolveLatLng(data.pincode)
: { latitude: existing?.latitude ?? null, longitude: existing?.longitude ?? null };
const contacts = parseContacts(formData);
await db.$transaction(async (tx) => {
await tx.vendor.update({
where: { id },
data: {
name: data.name,
vendorId: data.vendorId ?? null,
address: data.address ?? null,
pincode: data.pincode ?? null,
gstin: data.gstin ?? null,
latitude,
longitude,
isVerified: !!data.vendorId,
},
});
// Replace contacts wholesale
await tx.vendorContact.deleteMany({ where: { vendorId: id } });
if (contacts.length > 0) {
await tx.vendorContact.createMany({ data: contacts.map((c) => ({ ...c, vendorId: id })) });
}
});
revalidatePath("/admin/vendors");
revalidatePath(`/admin/vendors/${id}`);
return { ok: true };
}
export async function toggleVendorActive(vendorId: string): Promise<ActionResult> {
const session = await auth();
if (!session?.user || !hasPermission(session.user.role, "manage_vendors")) {
return { error: "Unauthorized" };
}
const vendor = await db.vendor.findUnique({ where: { id: vendorId }, select: { isActive: true } });
if (!vendor) return { error: "Vendor not found" };
await db.vendor.update({ where: { id: vendorId }, data: { isActive: !vendor.isActive } });
revalidatePath("/admin/vendors");
return { ok: true };
}
export async function deleteVendor(id: string): Promise<ActionResult> {
const session = await auth();
if (!session?.user || !hasPermission(session.user.role, "manage_vendors")) return { error: "Unauthorized" };
const blocked = await db.purchaseOrder.findFirst({
where: { vendorId: id, status: { not: "DRAFT" } },
});
if (blocked) return { error: "Cannot delete: vendor is referenced in submitted or active purchase orders." };
try {
await db.$transaction(
async (tx) => {
await tx.purchaseOrder.updateMany({ where: { vendorId: id, status: "DRAFT" }, data: { vendorId: null } });
await tx.product.updateMany({ where: { lastVendorId: id }, data: { lastVendorId: null } });
await tx.productVendorPrice.deleteMany({ where: { vendorId: id } });
await tx.vendor.delete({ where: { id } });
},
{ timeout: 30000 },
);
} catch (err: unknown) {
const code = (err as { code?: string })?.code;
if (code === "P2028") {
return { error: "Delete timed out — please try again." };
}
throw err;
}
revalidatePath("/admin/vendors");
return { ok: true };
}