feat: add Vendor validation + CRUD (src/lib/vendors.ts)

This commit is contained in:
toanhoang
2026-08-02 19:28:25 -04:00
parent 07ef1582d3
commit 19b652142a
+123
View File
@@ -0,0 +1,123 @@
import { z } from "zod";
import { prisma } from "./prisma";
export interface VendorRecord {
id: string;
name: string;
contact_email: string;
contact_phone: string;
address: string;
tax_code: string | null;
notes: string | null;
created_at: string;
}
export interface CreateVendorInput {
name: string;
contactEmail: string;
contactPhone: string;
address: string;
taxCode?: string | null;
notes?: string | null;
}
export type ValidationResult =
| { ok: true; value: CreateVendorInput }
| { ok: false; error: string };
// Mirrors src/lib/licenses.ts's nonEmptyString helper.
function nonEmptyString(message: string) {
return z.string({ required_error: message, invalid_type_error: message }).refine(
(value) => value.trim() !== "",
{ message }
);
}
const createVendorSchema = z.object({
name: nonEmptyString("name is required and must be a non-empty string"),
contactEmail: z
.string({
required_error: "contactEmail is required and must be a valid email",
invalid_type_error: "contactEmail is required and must be a valid email",
})
.email("contactEmail is required and must be a valid email"),
contactPhone: nonEmptyString("contactPhone is required and must be a non-empty string"),
address: nonEmptyString("address is required and must be a non-empty string"),
// Nullable, per docs/asset-license-management.md's "## Vendor" spec — not
// every vendor record has a known VN tax code at creation time.
taxCode: nonEmptyString("taxCode must be a non-empty string").optional().nullable(),
notes: nonEmptyString("notes must be a non-empty string").optional().nullable(),
});
/**
* Validates raw (untyped) vendor-creation input. name/contactPhone/address
* must be non-empty strings, contactEmail must be a valid email string,
* taxCode/notes are optional/nullable (per the "## Vendor" spec in
* docs/asset-license-management.md) but must be non-empty strings if
* present.
*/
export function validateCreateVendorInput(input: {
name?: unknown;
contactEmail?: unknown;
contactPhone?: unknown;
address?: unknown;
taxCode?: unknown;
notes?: unknown;
}): ValidationResult {
const result = createVendorSchema.safeParse(input);
if (!result.success) {
const message = result.error.issues[0]?.message ?? "Invalid input";
return { ok: false, error: message };
}
return { ok: true, value: result.data };
}
function toRecord(row: {
id: string;
name: string;
contactEmail: string;
contactPhone: string;
address: string;
taxCode: string | null;
notes: string | null;
createdAt: Date;
}): VendorRecord {
return {
id: row.id,
name: row.name,
contact_email: row.contactEmail,
contact_phone: row.contactPhone,
address: row.address,
tax_code: row.taxCode,
notes: row.notes,
created_at: row.createdAt.toISOString(),
};
}
/**
* Creates a Vendor. No unique constraint exists on any field, no FKs
* either (see docs/asset-license-management.md's "## Vendor" spec — this
* is a standalone entity, same starting shape as Employee/License before
* they grew relations) — so unlike createAssetItem()/createEmployee()
* there's no known Prisma error code to catch/translate here.
*/
export async function createVendor(input: CreateVendorInput): Promise<VendorRecord> {
const created = await prisma.vendor.create({
data: {
name: input.name,
contactEmail: input.contactEmail,
contactPhone: input.contactPhone,
address: input.address,
taxCode: input.taxCode ?? null,
notes: input.notes ?? null,
},
});
return toRecord(created);
}
export async function listVendors(): Promise<VendorRecord[]> {
const rows = await prisma.vendor.findMany({
orderBy: [{ createdAt: "desc" }, { id: "desc" }],
});
return rows.map(toRecord);
}