feat(kantine-datev): Kantinenabrechnung als Modul in neuer Gruppe Finanzbuchhaltung
- 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>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
import { BadRequestException, Injectable, UnprocessableEntityException } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { forTenant } from '../prisma/prisma-tenant.extension';
|
||||
import type { KantineDatevSettingsDto } from './dto/kantine-datev-settings.dto';
|
||||
import {
|
||||
buildKantineExport,
|
||||
KantineExportError,
|
||||
processKantineCsv,
|
||||
} from './kantine-datev.pipeline';
|
||||
import type { FileResponse, KantineDatevSettings, KantinePreview } from './kantine-datev.types';
|
||||
|
||||
export interface KantineSettingsResponse {
|
||||
beraterNr: string | null;
|
||||
mandantNr: string | null;
|
||||
lohnart: string | null;
|
||||
configured: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kantinenabrechnung (quick-261002-fm5). Die Einstellungen liegen je Mandant
|
||||
* in `KantineDatevConfig` (mandantengebunden); die hochgeladene CSV wird nur
|
||||
* im Arbeitsspeicher verarbeitet und weder gespeichert noch protokolliert.
|
||||
*/
|
||||
@Injectable()
|
||||
export class KantineDatevService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async getSettings(tenantId: string): Promise<KantineSettingsResponse> {
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId);
|
||||
const row = await tenantPrisma.kantineDatevConfig.findUnique({ where: { tenantId } });
|
||||
const beraterNr = row?.beraterNr ?? null;
|
||||
const mandantNr = row?.mandantNr ?? null;
|
||||
const lohnart = row?.lohnart ?? null;
|
||||
return {
|
||||
beraterNr,
|
||||
mandantNr,
|
||||
lohnart,
|
||||
configured: Boolean(beraterNr && mandantNr && lohnart),
|
||||
};
|
||||
}
|
||||
|
||||
async saveSettings(
|
||||
tenantId: string,
|
||||
dto: KantineDatevSettingsDto,
|
||||
): Promise<KantineSettingsResponse> {
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId);
|
||||
const data = { beraterNr: dto.beraterNr, mandantNr: dto.mandantNr, lohnart: dto.lohnart };
|
||||
await tenantPrisma.kantineDatevConfig.upsert({
|
||||
where: { tenantId },
|
||||
create: { tenantId, ...data },
|
||||
update: data,
|
||||
});
|
||||
return { ...data, configured: true };
|
||||
}
|
||||
|
||||
private async loadConfigured(tenantId: string): Promise<KantineDatevSettings | null> {
|
||||
const s = await this.getSettings(tenantId);
|
||||
if (!s.configured || !s.beraterNr || !s.mandantNr || !s.lohnart) return null;
|
||||
return { beraterNr: s.beraterNr, mandantNr: s.mandantNr, lohnart: s.lohnart };
|
||||
}
|
||||
|
||||
async preview(tenantId: string, buffer: Buffer): Promise<KantinePreview> {
|
||||
return processKantineCsv(buffer, await this.loadConfigured(tenantId));
|
||||
}
|
||||
|
||||
async export(tenantId: string, buffer: Buffer): Promise<FileResponse> {
|
||||
const settings = await this.loadConfigured(tenantId);
|
||||
try {
|
||||
return buildKantineExport(buffer, settings);
|
||||
} catch (error) {
|
||||
if (error instanceof KantineExportError) {
|
||||
if (error.code === 'qualityCheckFailed') {
|
||||
throw new UnprocessableEntityException({ code: error.code, message: error.message });
|
||||
}
|
||||
throw new BadRequestException({
|
||||
code: error.code,
|
||||
message: error.message,
|
||||
errors: error.errors,
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user