diff --git a/src/lib/vendors.ts b/src/lib/vendors.ts new file mode 100644 index 0000000..1763603 --- /dev/null +++ b/src/lib/vendors.ts @@ -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 { + 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 { + const rows = await prisma.vendor.findMany({ + orderBy: [{ createdAt: "desc" }, { id: "desc" }], + }); + return rows.map(toRecord); +}