feat: add CashboxExportService.exportForUser

This commit is contained in:
Bastian Wagner
2026-08-03 21:30:22 +02:00
parent f8857f71e1
commit 396dc29cf5
3 changed files with 123 additions and 0 deletions

View File

@@ -0,0 +1,60 @@
import { NotFoundException } from '@nestjs/common';
import { TeamRolesEnum } from '../team-roles/team-roles.enum';
import { CashboxExportService } from './cashbox-export.service';
describe('CashboxExportService', () => {
const teamRepository = { findOne: jest.fn() };
const access = { assertAtLeast: jest.fn() };
let service: CashboxExportService;
const team = {
id: 5,
name: 'Team A',
alias: 'team-a',
transactions: [
{ date: '2026-08-05T00:00:00.000Z', amount: 10, note: 'Sponsoring', type: { name: 'credit' } },
],
players: [],
};
beforeEach(() => {
jest.clearAllMocks();
teamRepository.findOne.mockResolvedValue(team);
service = new CashboxExportService(teamRepository as any, access as any);
});
it('checks permission before loading data', async () => {
await service.exportForUser(5, 42, '2026-08-01', '2026-08-31', 'csv');
expect(access.assertAtLeast).toHaveBeenCalledWith(
42,
5,
'transaction_create_min_role',
TeamRolesEnum.scnd_treasurer,
);
});
it('throws NotFoundException for an unknown team', async () => {
teamRepository.findOne.mockResolvedValue(null);
await expect(
service.exportForUser(999, 42, '2026-08-01', '2026-08-31', 'csv'),
).rejects.toBeInstanceOf(NotFoundException);
});
it('builds a CSV buffer with the correct content type and filename', async () => {
const result = await service.exportForUser(5, 42, '2026-08-01', '2026-08-31', 'csv');
expect(result.contentType).toBe('text/csv; charset=utf-8');
expect(result.filename).toBe('kassenbuch_team-a_2026-08-01_2026-08-31.csv');
expect(result.buffer.toString('utf-8')).toContain('Sponsoring');
});
it('builds a PDF buffer with the correct content type and filename', async () => {
const result = await service.exportForUser(5, 42, '2026-08-01', '2026-08-31', 'pdf');
expect(result.contentType).toBe('application/pdf');
expect(result.filename).toBe('kassenbuch_team-a_2026-08-01_2026-08-31.pdf');
expect(result.buffer.subarray(0, 5).toString('utf-8')).toBe('%PDF-');
});
});

View File

@@ -0,0 +1,51 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { TeamRolesEnum } from 'src/team-roles/team-roles.enum';
import { Team } from 'src/teams/entities/team.entity';
import { TeamAccessService } from 'src/teams/team-access.service';
import { Repository } from 'typeorm';
import { buildCsv, buildPdf, buildRows } from './cashbox-export.utils';
@Injectable()
export class CashboxExportService {
constructor(
@InjectRepository(Team)
private readonly teamRepository: Repository<Team>,
private readonly access: TeamAccessService,
) {}
async exportForUser(
teamId: number,
userId: number,
from: string,
to: string,
format: 'csv' | 'pdf',
): Promise<{ buffer: Buffer; contentType: string; filename: string }> {
await this.access.assertAtLeast(
userId,
teamId,
'transaction_create_min_role',
TeamRolesEnum.scnd_treasurer,
);
const team = await this.teamRepository.findOne({
where: { id: teamId },
relations: ['players', 'players.transactions', 'transactions'],
});
if (!team) throw new NotFoundException('Team nicht gefunden.');
const rows = buildRows(team, from, to);
if (format === 'csv') {
return {
buffer: Buffer.from(buildCsv(rows), 'utf-8'),
contentType: 'text/csv; charset=utf-8',
filename: `kassenbuch_${team.alias}_${from}_${to}.csv`,
};
}
return {
buffer: await buildPdf(team, rows, from, to),
contentType: 'application/pdf',
filename: `kassenbuch_${team.alias}_${from}_${to}.pdf`,
};
}
}

View File

@@ -0,0 +1,12 @@
import { IsDateString, IsIn } from 'class-validator';
export class CashboxExportQueryDto {
@IsDateString()
from: string;
@IsDateString()
to: string;
@IsIn(['csv', 'pdf'])
format: 'csv' | 'pdf';
}