import { z } from "zod";
import { defineContract } from "@withboundary/contract";
const lineItemSchema = z.object({
description: z.string(),
quantity: z.number(),
unitPrice: z.number(),
amount: z.number(),
});
export const invoiceSchema = z.object({
vendor: z.string(),
invoiceNumber: z.string(),
date: z.string(),
lineItems: z.array(lineItemSchema),
subtotal: z.number(),
tax: z.number(),
total: z.number(),
currency: z.string(),
});
export const invoiceContract = defineContract({
name: "invoice-extraction",
schema: invoiceSchema,
rules: [
{
name: "has_line_items",
description: "Every invoice has at least one line item",
fields: ["lineItems"],
check: (invoice) =>
invoice.lineItems.length > 0 || "invoice must include at least one line item",
},
{
name: "line_items_sum_to_subtotal",
description: "Line item amounts sum to subtotal",
fields: ["lineItems", "subtotal"],
check: (invoice) => {
const sum = invoice.lineItems.reduce((total, item) => total + item.amount, 0);
return Math.abs(sum - invoice.subtotal) < 0.01
|| `line items sum to ${sum}, but subtotal is ${invoice.subtotal}`;
},
},
{
name: "subtotal_plus_tax_equals_total",
description: "Subtotal plus tax equals total within a cent",
fields: ["subtotal", "tax", "total"],
check: (invoice) => {
const expected = invoice.subtotal + invoice.tax;
return Math.abs(expected - invoice.total) < 0.01
|| `subtotal ${invoice.subtotal} plus tax ${invoice.tax} is ${expected}, but total is ${invoice.total}`;
},
},
{
name: "line_item_math_consistent",
description: "Each line item amount equals quantity times unit price",
fields: ["lineItems"],
check: (invoice) => {
const bad = invoice.lineItems.find(
(item) => Math.abs(item.quantity * item.unitPrice - item.amount) >= 0.01,
);
return !bad
|| `${bad.description}: ${bad.quantity} x ${bad.unitPrice} should equal ${bad.quantity * bad.unitPrice}, but amount is ${bad.amount}`;
},
},
],
});