Add comprehensive tests for client validation, revenue and expense categories, invoice generation, and schemas
- Implement tests for client validation functions including tax ID, VAT ID, IBAN, BIC, website, and company form validation. - Create tests for revenue and expense categories ensuring all expected categories and labels are present. - Add tests for invoice number generation with various scenarios including prefix handling and sequence padding. - Introduce tests for default categories and their integration, ensuring no overlaps and consistent naming conventions. - Implement Zod schema validation tests for currency, tax rates, IBAN, tax ID, VAT ID, invoices, companies, and customers. - Add utility tests for tax calculations, including item amounts and invoice totals, ensuring correct handling of tax rates and formatting. - Set up Vitest configuration and global test setup for consistent testing environment.
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { Decimal } from "@prisma/client";
|
||||
|
||||
describe("Buchungen - Double-Entry Bookkeeping Logic", () => {
|
||||
describe("TransactionAccount Enum", () => {
|
||||
it("should have KASSE and BANK accounts", () => {
|
||||
// These are the two main accounts for German bookkeeping
|
||||
const accounts = ["KASSE", "BANK"];
|
||||
expect(accounts).toContain("KASSE");
|
||||
expect(accounts).toContain("BANK");
|
||||
});
|
||||
});
|
||||
|
||||
describe("TransactionType Enum", () => {
|
||||
it("should have EINLAGE and ENTNAHME types", () => {
|
||||
// EINLAGE = Owner investment, ENTNAHME = Owner withdrawal
|
||||
const types = ["EINLAGE", "ENTNAHME"];
|
||||
expect(types).toContain("EINLAGE");
|
||||
expect(types).toContain("ENTNAHME");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Zahlungsart Enum", () => {
|
||||
it("should have KASSE and BANK payment methods", () => {
|
||||
const paymentMethods = ["KASSE", "BANK"];
|
||||
expect(paymentMethods).toContain("KASSE");
|
||||
expect(paymentMethods).toContain("BANK");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Buchung Business Logic", () => {
|
||||
it("should calculate correct sign for EINLAGE (positive)", () => {
|
||||
// EINLAGE increases the company's assets
|
||||
const amount = 1000;
|
||||
const type = "EINLAGE";
|
||||
|
||||
// In German bookkeeping: EINLAGE is recorded as positive (credit to equity)
|
||||
const signedAmount = type === "EINLAGE" ? amount : -amount;
|
||||
expect(signedAmount).toBe(1000);
|
||||
});
|
||||
|
||||
it("should calculate correct sign for ENTNAHME (negative)", () => {
|
||||
// ENTNAHME decreases the company's assets
|
||||
const amount = 500;
|
||||
const type = "ENTNAHME";
|
||||
|
||||
// In German bookkeeping: ENTNAHME is recorded as negative (debit to equity)
|
||||
const signedAmount = type === "EINLAGE" ? amount : -amount;
|
||||
expect(signedAmount).toBe(-500);
|
||||
});
|
||||
|
||||
it("should identify business records correctly", () => {
|
||||
// Business records (isBusinessRecord = true) come from Einnahmen/Ausgaben
|
||||
const isBusinessRecord = true;
|
||||
const hasKategorie = true;
|
||||
|
||||
expect(isBusinessRecord).toBe(true);
|
||||
expect(hasKategorie).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle non-business records (private)", () => {
|
||||
// Non-business records might not have a kategorie
|
||||
const isBusinessRecord = false;
|
||||
const kategorie = null;
|
||||
|
||||
expect(isBusinessRecord).toBe(false);
|
||||
expect(kategorie).toBeNull();
|
||||
});
|
||||
|
||||
it("should validate Decimal precision for amounts", () => {
|
||||
// Prisma Decimal(10,2) - max 10 digits, 2 decimal places
|
||||
const amount = 12345678.90; // 8 digits before decimal, 2 after
|
||||
const maxAmount = 99999999.99; // Max for DECIMAL(10,2)
|
||||
|
||||
expect(amount).toBeLessThanOrEqual(maxAmount);
|
||||
expect(Number(amount.toFixed(2))).toBe(12345678.9);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Buchung Link Logic (Linked Transactions)", () => {
|
||||
it("should allow linking related transactions", () => {
|
||||
// Example: An invoice payment might be linked to the invoice
|
||||
const buchungId = "buchung-123";
|
||||
const linkedBuchungId = "buchung-456";
|
||||
|
||||
// A Buchung can be linked to another (e.g., invoice payment -> invoice)
|
||||
const link = { source: buchungId, target: linkedBuchungId };
|
||||
expect(link.source).toBe("buchung-123");
|
||||
expect(link.target).toBe("buchung-456");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Kategorie Logic", () => {
|
||||
it("should have unique category names per company", () => {
|
||||
// BuchungKategorie has @@unique([companyId, name, typ])
|
||||
const companyId = "company-123";
|
||||
const categories = [
|
||||
{ companyId, name: "Fußpflege", typ: "EINNAHME" },
|
||||
{ companyId, name: "Miete", typ: "AUSGABE" },
|
||||
];
|
||||
|
||||
const uniqueCheck = new Set(categories.map(c => `${c.companyId}-${c.name}-${c.typ}`));
|
||||
expect(uniqueCheck.size).toBe(categories.length);
|
||||
});
|
||||
|
||||
it("should distinguish between EINNAHME and AUSGABE", () => {
|
||||
const einnahme: "EINNAHME" = "EINNAHME";
|
||||
const ausgabe: "AUSGABE" = "AUSGABE";
|
||||
|
||||
expect(einnahme).toBe("EINNAHME");
|
||||
expect(ausgabe).toBe("AUSGABE");
|
||||
expect(einnahme).not.toBe(ausgabe);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Date-Based Queries", () => {
|
||||
it("should filter Buchungen by date range", () => {
|
||||
const buchungen = [
|
||||
{ date: new Date("2026-01-15"), amount: 100 },
|
||||
{ date: new Date("2026-02-20"), amount: 200 },
|
||||
{ date: new Date("2026-03-10"), amount: 300 },
|
||||
];
|
||||
|
||||
const startDate = new Date("2026-02-01");
|
||||
const endDate = new Date("2026-03-31");
|
||||
|
||||
const filtered = buchungen.filter(b =>
|
||||
b.date >= startDate && b.date <= endDate
|
||||
);
|
||||
|
||||
expect(filtered).toHaveLength(2);
|
||||
expect(filtered[0].amount).toBe(200);
|
||||
expect(filtered[1].amount).toBe(300);
|
||||
});
|
||||
|
||||
it("should group Buchungen by month for reports", () => {
|
||||
const buchungen = [
|
||||
{ date: new Date("2026-01-10"), amount: 100 },
|
||||
{ date: new Date("2026-01-20"), amount: 150 },
|
||||
{ date: new Date("2026-02-05"), amount: 200 },
|
||||
];
|
||||
|
||||
const grouped = buchungen.reduce((acc, b) => {
|
||||
const month = b.date.getMonth(); // 0-indexed
|
||||
acc[month] = (acc[month] || 0) + b.amount;
|
||||
return acc;
|
||||
}, {} as Record<number, number>);
|
||||
|
||||
expect(grouped[0]).toBe(250); // January (month 0)
|
||||
expect(grouped[1]).toBe(200); // February (month 1)
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user