1f85277a3b
- neue Seitenleisten-Kategorie accounting (Finanzbuchhaltung / Financial accounting) - CSV (UTF-8, UTF-8 mit BOM, Windows-1252) pruefen, Vorschau mit Zeilenfehlern, DATEV-Lohn-ASCII-Export - Beraternummer, Mandantennummer, Lohnart je Mandant als Admin-Einstellung (KantineDatevConfig mit Zeilenschutz), anfangs leer - hochgeladene Daten werden nicht gespeichert; Download per Blob (auch im Desktop-Client) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
110 lines
3.9 KiB
TypeScript
110 lines
3.9 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
buildKantineExport,
|
|
KantineExportError,
|
|
processKantineCsv,
|
|
} from './kantine-datev.pipeline';
|
|
|
|
const SETTINGS = { beraterNr: '1234567', mandantNr: '12345', lohnart: '1111' };
|
|
const HEADER = 'PersNr;Name;Menge;EK;Netto;ZuAb;MwSt;Zuschuss;Betrag;Von;Bis';
|
|
const ok = (nr: string, name: string, betrag: string, bis = '31.03.2026') =>
|
|
`${nr};${name};1;1,00;1,00;0;0;0;${betrag};01.03.2026;${bis}`;
|
|
|
|
function csv(lines: string[], eol = '\r\n'): string {
|
|
return [HEADER, ...lines].join(eol);
|
|
}
|
|
|
|
describe('processKantineCsv', () => {
|
|
it('liest Windows-1252 mit Umlauten und CRLF', () => {
|
|
const buf = Buffer.from(
|
|
csv([ok('100', 'Müller, Jürgen', '7,94'), ok('200', 'Köhler', '51,5')]),
|
|
'latin1',
|
|
);
|
|
const p = processKantineCsv(buf, SETTINGS);
|
|
expect(p.rowCount).toBe(2);
|
|
expect(p.abrechnungsMonat).toBe('03/2026');
|
|
expect(p.totalCents).toBe(794 + 5150);
|
|
expect(p.errors).toEqual([]);
|
|
expect(p.canExport).toBe(true);
|
|
expect(p.blockedReason).toBeNull();
|
|
});
|
|
|
|
it('liest UTF-8 mit BOM und LF gleich', () => {
|
|
const body = csv([ok('100', 'Müller', '7,94')], '\n');
|
|
const buf = Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), Buffer.from(body, 'utf8')]);
|
|
const p = processKantineCsv(buf, SETTINGS);
|
|
expect(p.rowCount).toBe(1);
|
|
expect(p.errors).toEqual([]);
|
|
});
|
|
|
|
it('zaehlt in der Summe nur Zeilen mit gueltigem Betrag', () => {
|
|
const p = processKantineCsv(
|
|
Buffer.from(csv([ok('100', 'A', '7,94'), ok('101', 'B', 'abc')])),
|
|
SETTINGS,
|
|
);
|
|
expect(p.totalCents).toBe(794);
|
|
expect(p.errors).toEqual([expect.objectContaining({ row: 3, code: 'betragFormat' })]);
|
|
expect(p.canExport).toBe(false);
|
|
expect(p.blockedReason).toBe('errors');
|
|
});
|
|
|
|
it('meldet zwei Monate als Warnung, nicht als Fehler', () => {
|
|
const p = processKantineCsv(
|
|
Buffer.from(csv([ok('100', 'A', '1,00'), `101;B;1;1;1;0;0;0;1,00;01.04.2026;30.04.2026`])),
|
|
SETTINGS,
|
|
);
|
|
expect(p.warnings).toHaveLength(1);
|
|
expect(p.errors).toEqual([]);
|
|
expect(p.abrechnungsMonat).toBe('03/2026');
|
|
});
|
|
|
|
it('meldet eine Datei ohne Datenzeilen mit noRows', () => {
|
|
const p = processKantineCsv(Buffer.from(csv([])), SETTINGS);
|
|
expect(p.rowCount).toBe(0);
|
|
expect(p.errors.map((e) => e.code)).toEqual(['noRows']);
|
|
expect(p.canExport).toBe(false);
|
|
});
|
|
|
|
it('sperrt ohne Einstellungen mit settingsMissing', () => {
|
|
const p = processKantineCsv(Buffer.from(csv([ok('100', 'A', '1,00')])), null);
|
|
expect(p.canExport).toBe(false);
|
|
expect(p.blockedReason).toBe('settingsMissing');
|
|
});
|
|
|
|
it('gibt keine Zeileninhalte in der Vorschau zurueck', () => {
|
|
const p = processKantineCsv(Buffer.from(csv([ok('100', 'Geheimname', 'x')])), SETTINGS);
|
|
expect(JSON.stringify(p)).not.toContain('Geheimname');
|
|
});
|
|
});
|
|
|
|
describe('buildKantineExport', () => {
|
|
it('erzeugt Dateiname und Base64-Inhalt', () => {
|
|
const r = buildKantineExport(Buffer.from(csv([ok('100', 'A', '7,94')])), SETTINGS);
|
|
expect(r.filename).toBe('LuG_1234567_12345_03_2026.sic');
|
|
expect(r.mimeType).toBe('text/plain');
|
|
expect(Buffer.from(r.content, 'base64').toString('utf8')).toBe(
|
|
'1234567\t12345\t03/2026\t\t\t\t\t\t\t\t\r\n\t100\t\t1111\t-7.94\t\t\t\t\t\t\r\n',
|
|
);
|
|
});
|
|
|
|
it('verweigert den Export bei Fehlern', () => {
|
|
expect(() => buildKantineExport(Buffer.from(csv([ok('x', 'A', '7,94')])), SETTINGS)).toThrow(
|
|
KantineExportError,
|
|
);
|
|
try {
|
|
buildKantineExport(Buffer.from(csv([ok('x', 'A', '7,94')])), SETTINGS);
|
|
} catch (e) {
|
|
expect((e as KantineExportError).code).toBe('hasErrors');
|
|
}
|
|
});
|
|
|
|
it('verweigert den Export ohne Einstellungen', () => {
|
|
try {
|
|
buildKantineExport(Buffer.from(csv([ok('100', 'A', '7,94')])), null);
|
|
expect.unreachable();
|
|
} catch (e) {
|
|
expect((e as KantineExportError).code).toBe('settingsMissing');
|
|
}
|
|
});
|
|
});
|