fix: Berechtigungen der Tagesübersicht, WebDAV-Ausbau, Tests instand gesetzt
Build and Push Multi-Platform Images / build-and-push (push) Successful in 55s
Build and Push Multi-Platform Images / build-and-push (push) Successful in 55s
Die tägliche E-Mail-Zusammenfassung zeigte Nutzern Bereiche, für die sie
keine Berechtigung haben. Zwei unabhängige Ursachen:
- Das Backend prüfte weiterhin die Altgruppe PM_Belege. Die Umbenennung
zu PM_Buchhaltung (ca1d371) war nur im Frontend angekommen, weshalb
der Digest "Manuell bearbeiten" und "In Agrarmonitor" anbot, während
die Oberfläche beide Bereiche sperrte.
- Der Cron-Versand wertet die Gruppen aus user_settings aus. Diese Spalte
wurde nur beim Aufruf der Benutzereinstellungen gefüllt; entzogene
Berechtigungen erreichten den Digest daher unter Umständen nie.
Behoben durch Angleichen des Gruppen-Mappings und den neuen
UserIdentitySyncService, der E-Mail, Benutzername und Gruppen bei jedem
authentifizierten Request aus dem Token spiegelt – ohne den Request zu
blockieren und ohne DB-Zugriff, solange sich das Token nicht ändert. Die
doppelte Identitätspflege im UserSettingsService entfällt.
WebDAV wird nicht eingesetzt und ist entfernt; Export-Ziele bieten nur
noch FTP. Damit verschwindet das ESM-Paket webdav, an dem zwei
Jest-Suites bereits beim Parsen scheiterten.
Veraltete Tests instand gesetzt: email.controller und settings.controller
mockten weniger Abhängigkeiten, als die Klassen inzwischen haben;
postprocessing.service.spec beschrieb noch das alte Regelmodell mit
Einzelfeldern statt FilterJson und ist gegen die heutige Filter-Engine
neu geschrieben (AND/OR, verschachtelte Gruppen, Fehlerprotokollierung).
Enthält außerdem den Arbeitsstand der E-Rechnungs-Mandantenzuordnung, da
sich beide Änderungen dieselben Dateien teilen (SettingsPage, package.json).
98 Tests in 12 Suites grün, Backend- und Frontend-Build sauber.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
32ba772e4e
commit
b7b65c64ae
41 files changed
+2462
-443
No files matched your search
@@ -7,6 +7,14 @@ import { PostprocessingAction } from '../database/entities/postprocessing-action
|
||||
import { UserClient } from '../database/entities/user-client.entity';
|
||||
import { Client } from '../database/entities/client.entity';
|
||||
import { Setting } from '../database/entities/setting.entity';
|
||||
import { PostprocessingLog } from '../database/entities/postprocessing-log.entity';
|
||||
import { ExportTarget } from '../database/entities/export-target.entity';
|
||||
import { ClientIdentifier } from '../database/entities/client-identifier.entity';
|
||||
import { DocumentField } from '../database/entities/document-field.entity';
|
||||
import { CorrespondentSetting } from '../database/entities/correspondent-setting.entity';
|
||||
import { InboxPostprocessingAction } from '../database/entities/inbox-postprocessing-action.entity';
|
||||
import { PaperlessService } from '../paperless/paperless.service';
|
||||
import { ExportService } from '../postprocessing/export.service';
|
||||
|
||||
const makeRepo = (data: any[] = []) => ({
|
||||
find: jest.fn().mockResolvedValue(data),
|
||||
@@ -23,8 +31,10 @@ describe('SettingsController', () => {
|
||||
let ppRepo: ReturnType<typeof makeRepo>;
|
||||
let ppActionRepo: ReturnType<typeof makeRepo>;
|
||||
let userClientRepo: ReturnType<typeof makeRepo>;
|
||||
let paperlessService: { getDocumentTypes: jest.Mock };
|
||||
|
||||
beforeEach(async () => {
|
||||
paperlessService = { getDocumentTypes: jest.fn().mockResolvedValue([]) };
|
||||
docTypeRepo = makeRepo([{ Id: 1, DocumentTypeId: 10, TitelTemplate: 'T' }]);
|
||||
ppRepo = makeRepo([{ Id: 1, Name: 'Rule1', Order: 1, IsActive: true }]);
|
||||
ppActionRepo = makeRepo([]);
|
||||
@@ -45,6 +55,23 @@ describe('SettingsController', () => {
|
||||
provide: getRepositoryToken(Setting),
|
||||
useValue: makeRepo([{ ID: 1, Typ: 1, Wert: 'v' }]),
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(PostprocessingLog),
|
||||
useValue: makeRepo(),
|
||||
},
|
||||
{ provide: getRepositoryToken(ExportTarget), useValue: makeRepo() },
|
||||
{ provide: getRepositoryToken(ClientIdentifier), useValue: makeRepo() },
|
||||
{ provide: getRepositoryToken(DocumentField), useValue: makeRepo() },
|
||||
{
|
||||
provide: getRepositoryToken(CorrespondentSetting),
|
||||
useValue: makeRepo(),
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(InboxPostprocessingAction),
|
||||
useValue: makeRepo(),
|
||||
},
|
||||
{ provide: PaperlessService, useValue: paperlessService },
|
||||
{ provide: ExportService, useValue: { testConnection: jest.fn() } },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@@ -62,6 +89,30 @@ describe('SettingsController', () => {
|
||||
expect(docTypeRepo.find).toHaveBeenCalledWith({ order: { Id: 'ASC' } });
|
||||
});
|
||||
|
||||
it('getDocumentTypes legt in Paperless neu angelegte Typen an', async () => {
|
||||
paperlessService.getDocumentTypes.mockResolvedValue([
|
||||
{ id: 10, name: 'Bekannt' },
|
||||
{ id: 11, name: 'Neu' },
|
||||
]);
|
||||
|
||||
await controller.getDocumentTypes();
|
||||
|
||||
expect(docTypeRepo.create).toHaveBeenCalledTimes(1);
|
||||
expect(docTypeRepo.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ DocumentTypeId: 11 }),
|
||||
);
|
||||
expect(docTypeRepo.save).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('getDocumentTypes liefert die Liste auch bei Paperless-Ausfall', async () => {
|
||||
paperlessService.getDocumentTypes.mockRejectedValue(new Error('down'));
|
||||
|
||||
const result = await controller.getDocumentTypes();
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(docTypeRepo.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('updateDocumentType calls update + findOneByOrFail', async () => {
|
||||
await controller.updateDocumentType('1', { TitelTemplate: 'New' });
|
||||
expect(docTypeRepo.update).toHaveBeenCalledWith(1, {
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
Body,
|
||||
Query,
|
||||
Logger,
|
||||
BadRequestException,
|
||||
ConflictException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
@@ -20,6 +22,11 @@ import { ExportTarget } from '../database/entities/export-target.entity';
|
||||
import { UserClient } from '../database/entities/user-client.entity';
|
||||
import { PaperlessService } from '../paperless/paperless.service';
|
||||
import { Client } from '../database/entities/client.entity';
|
||||
import {
|
||||
ClientIdentifier,
|
||||
ClientIdentifierTyp,
|
||||
CLIENT_IDENTIFIER_TYPEN,
|
||||
} from '../database/entities/client-identifier.entity';
|
||||
import { Setting } from '../database/entities/setting.entity';
|
||||
import { CorrespondentSetting } from '../database/entities/correspondent-setting.entity';
|
||||
import {
|
||||
@@ -29,6 +36,7 @@ import {
|
||||
import { ExportService } from '../postprocessing/export.service';
|
||||
import { RequirePermissions } from '../auth/permissions.decorator';
|
||||
import { Permission } from '../auth/permissions.enum';
|
||||
import { normalisiereKennung } from '../erechnung/mandant-matcher.service';
|
||||
|
||||
@Controller('api/settings')
|
||||
@RequirePermissions(Permission.MANAGE_SETTINGS)
|
||||
@@ -49,6 +57,8 @@ export class SettingsController {
|
||||
@InjectRepository(UserClient)
|
||||
private readonly userClientRepo: Repository<UserClient>,
|
||||
@InjectRepository(Client) private readonly clientRepo: Repository<Client>,
|
||||
@InjectRepository(ClientIdentifier)
|
||||
private readonly clientIdentifierRepo: Repository<ClientIdentifier>,
|
||||
@InjectRepository(Setting)
|
||||
private readonly settingRepo: Repository<Setting>,
|
||||
@InjectRepository(DocumentField)
|
||||
@@ -520,4 +530,56 @@ export class SettingsController {
|
||||
});
|
||||
return this.clientRepo.findOneByOrFail({ Id: clientId });
|
||||
}
|
||||
|
||||
// === E-Rechnungs-Kennungen der Mandanten ===
|
||||
@Get('client-identifiers')
|
||||
async getClientIdentifiers() {
|
||||
return this.clientIdentifierRepo.find({
|
||||
order: { ClientId: 'ASC', Typ: 'ASC', Wert: 'ASC' },
|
||||
});
|
||||
}
|
||||
|
||||
@Post('client-identifiers')
|
||||
async createClientIdentifier(
|
||||
@Body() body: { ClientId: number; Typ: ClientIdentifierTyp; Wert: string },
|
||||
) {
|
||||
const clientId = Number(body.ClientId);
|
||||
if (!clientId || isNaN(clientId)) {
|
||||
throw new BadRequestException('Mandant ist erforderlich');
|
||||
}
|
||||
if (!CLIENT_IDENTIFIER_TYPEN.includes(body.Typ)) {
|
||||
throw new BadRequestException('Unbekannte Kennungsart');
|
||||
}
|
||||
|
||||
// Gleiche Normalisierung wie beim Matching, sonst greift die Zuordnung nie.
|
||||
const wert = normalisiereKennung(body.Wert);
|
||||
if (!wert) throw new BadRequestException('Kennung ist erforderlich');
|
||||
if (wert.length > 100) {
|
||||
throw new BadRequestException('Kennung ist zu lang (max. 100 Zeichen)');
|
||||
}
|
||||
|
||||
const vorhanden = await this.clientIdentifierRepo.findOneBy({
|
||||
Typ: body.Typ,
|
||||
Wert: wert,
|
||||
});
|
||||
if (vorhanden) {
|
||||
throw new ConflictException(
|
||||
'Diese Kennung ist bereits einem Mandanten zugeordnet.',
|
||||
);
|
||||
}
|
||||
|
||||
return this.clientIdentifierRepo.save(
|
||||
this.clientIdentifierRepo.create({
|
||||
ClientId: clientId,
|
||||
Typ: body.Typ,
|
||||
Wert: wert,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@Delete('client-identifiers/:id')
|
||||
async deleteClientIdentifier(@Param('id') id: string) {
|
||||
await this.clientIdentifierRepo.delete(parseInt(id, 10));
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { PostprocessingLog } from '../database/entities/postprocessing-log.entit
|
||||
import { ExportTarget } from '../database/entities/export-target.entity';
|
||||
import { UserClient } from '../database/entities/user-client.entity';
|
||||
import { Client } from '../database/entities/client.entity';
|
||||
import { ClientIdentifier } from '../database/entities/client-identifier.entity';
|
||||
import { Setting } from '../database/entities/setting.entity';
|
||||
import { DocumentField } from '../database/entities/document-field.entity';
|
||||
import { CorrespondentSetting } from '../database/entities/correspondent-setting.entity';
|
||||
@@ -25,6 +26,7 @@ import { PostprocessingModule } from '../postprocessing/postprocessing.module';
|
||||
ExportTarget,
|
||||
UserClient,
|
||||
Client,
|
||||
ClientIdentifier,
|
||||
Setting,
|
||||
DocumentField,
|
||||
CorrespondentSetting,
|
||||
|
||||
Reference in new issue
Block a user