import assert from "node:assert/strict" import test from "node:test" import { getCreatedDocumentGrossByRate, getCreatedDocumentGrossTotal, getIncomingInvoiceGrossByRate, normalizeCashDiscountAccountsInput, resolveCashDiscountAccounts, splitCashDiscount, validateCashDiscount, } from "../src/modules/cash-discount" import { getCashDiscountBookingLines } from "../src/utils/export/datev" import { prepareStatementAllocationInput } from "../src/mcp/statementAllocations" const defaults = resolveCashDiscountAccounts({}) test("splits incoming invoice discount by tax rate onto received discount accounts", () => { const gross = getIncomingInvoiceGrossByRate({ expense: true, accounts: [ { taxType: "19", amountNet: 100, amountTax: 19 }, { taxType: "7", amountNet: 100, amountTax: 7 }, ], }) assert.deepEqual(gross, { "19": -119, "7": -107, "0": 0 }) const split = splitCashDiscount(-4.52, gross, defaults.received) assert.deepEqual(split, [ { rate: 19, account: "3736", gross: -2.38, net: -2, tax: -0.38 }, { rate: 7, account: "3731", gross: -2.14, net: -2, tax: -0.14 }, ]) }) test("assigns rounding differences to the largest share", () => { const split = splitCashDiscount(1, { "19": 1, "7": 1, "0": 1 }, defaults.granted) assert.equal(split.reduce((sum, item) => sum + item.gross, 0).toFixed(2), "1.00") assert.equal(split.length, 3) }) test("rejects discounts on intra-community acquisitions", () => { assert.throws( () => getIncomingInvoiceGrossByRate({ accounts: [{ taxType: "19I", amountNet: 100, amountTax: 0 }] }), /manuell buchen/, ) }) test("final invoices spread discounts over the remaining claim after advance invoices", () => { const advance = { id: 7, rows: [{ advanceInvoiceData: true, price: 1000, taxPercent: 19 }] } const final = { id: 8, usedAdvanceInvoices: [7], rows: [ { mode: "normal", quantity: 1, price: 2000, discountPercent: 0, taxPercent: 19 }, { mode: "normal", quantity: 1, price: 1000, discountPercent: 0, taxPercent: 7 }, ], } assert.deepEqual(getCreatedDocumentGrossByRate(final, [advance]), { "19": 1190, "7": 1070, "0": 0 }) assert.equal(getCreatedDocumentGrossTotal(final, [advance]), 2260) }) test("tax exempt small business invoices use the account without tax", () => { const gross = getCreatedDocumentGrossByRate({ taxType: "19 UStG", rows: [{ mode: "normal", quantity: 1, price: 100, discountPercent: 0, taxPercent: 19 }], }) assert.deepEqual(gross, { "19": 0, "7": 0, "0": 100 }) assert.deepEqual(splitCashDiscount(2, gross, defaults.granted), [{ rate: 0, account: "8730", gross: 2, net: 2, tax: 0 }]) }) test("validates payment plus discount against the open amount", () => { assert.doesNotThrow(() => validateCashDiscount({ amount: 98, discount: 2, total: 100, settled: 0 })) assert.doesNotThrow(() => validateCashDiscount({ amount: -98, discount: -2, total: -100, settled: 0 })) assert.throws(() => validateCashDiscount({ amount: 99, discount: 2, total: 100, settled: 0 }), /offenen Betrag/) assert.throws(() => validateCashDiscount({ amount: 50, discount: 2, total: 100, settled: 50 }), /offenen Betrag/) assert.throws(() => validateCashDiscount({ amount: 80, discount: 20, total: 100, settled: 0 }), /höher als 10 %/) assert.throws(() => validateCashDiscount({ amount: -98, discount: -2, total: 100, settled: 0 }), /Richtung/) }) test("normalizes configured discount accounts and keeps SKR03 defaults", () => { const input = normalizeCashDiscountAccountsInput({ received: { "19": " 3737 ", "7": "" } }) assert.deepEqual(input, { received: { "19": "3737" } }) assert.equal(resolveCashDiscountAccounts(input).received["19"], "3737") assert.equal(resolveCashDiscountAccounts(input).received["7"], "3731") assert.throws(() => normalizeCashDiscountAccountsInput({ granted: { "19": "87A6" } }), /Ziffern/) }) test("exports discount bookings per tax rate against the business partner", () => { const lines = getCashDiscountBookingLines({ bankstatement: { id: 42, date: "2026-10-05" }, incominginvoice: { reference: "RE-1", vendor: { vendorNumber: "70001", name: "Lieferant" } }, cashDiscountSplit: [ { rate: 19, account: "3736", gross: -2.38, net: -2, tax: -0.38 }, { rate: 7, account: "3731", gross: -2.14, net: -2, tax: -0.14 }, ], }).map(line => line.split(";")) assert.equal(lines.length, 2) assert.equal(lines[0].length, 124) assert.deepEqual([lines[0][0], lines[0][1], lines[0][6], lines[0][7], lines[0][9], lines[0][10]], ["2,38", '"H"', "3736", "70001", "0510", '"RE-1"']) const [granted] = getCashDiscountBookingLines({ bankstatement: { id: 43, date: "2026-10-05" }, createddocument: { documentNumber: "RE-100", customer: { customerNumber: "10001", name: "Kunde" } }, cashDiscountSplit: [{ rate: 19, account: "8736", gross: 2.38, net: 2, tax: 0.38 }], }).map(line => line.split(";")) assert.deepEqual([granted[0], granted[1], granted[6], granted[7]], ["2,38", '"S"', "8736", "10001"]) }) test("MCP allocation input accepts cash discounts only with bank statements", () => { const payload = prepareStatementAllocationInput({ bankstatement: 1, createddocument: 2, amount: 98, cashDiscountAmount: 2 }) assert.equal(payload.cashDiscountAmount, 2) assert.throws(() => prepareStatementAllocationInput({ amount: 98, cashDiscountAmount: 2, manualBookingDate: "2026-10-01", account: 1, contraAccount: 2, }), /Bankzahlung/) })