29619658fc
Build and Push Docker Image / build (push) Failing after 1m35s
- Introduced a new graphify OpenCode plugin to remind users about the knowledge graph before executing bash commands. - Added AGENTS.md for agent guidance, including development commands, environment setup, Docker deployment, code structure, conventions, and testing instructions. - Created opencode.json to configure the graphify plugin and superpowers. - Updated tests to improve type safety and added missing imports in test files. - Added .graphify_uncached.txt to track relevant files for graphify.
153 lines
5.3 KiB
TypeScript
153 lines
5.3 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
|
|
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" | "ENTNAHME" = "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: "EINLAGE" | "ENTNAHME" = "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)
|
|
});
|
|
});
|
|
});
|