feat: add CashboxExportService.exportForUser
This commit is contained in:
@@ -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-');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user