feat: add CashboxExportSubscription entity and service

This commit is contained in:
Bastian Wagner
2026-08-03 21:41:56 +02:00
parent 009aae1f2b
commit a11eb73ce9
5 changed files with 270 additions and 0 deletions

View File

@@ -0,0 +1,99 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { TeamRolesEnum } from 'src/team-roles/team-roles.enum';
import { TeamAccessService } from 'src/teams/team-access.service';
import { Repository } from 'typeorm';
import { RecurringTransactionIntervalEnum } from 'src/recurring-transactions/recurring-transaction-interval.enum';
import { CashboxExportSubscriptionResponseDTO } from './dto/cashbox-export-subscription-response.dto';
import { UpsertCashboxExportSubscriptionDTO } from './dto/upsert-cashbox-export-subscription.dto';
import { CashboxExportSubscription } from './entities/cashbox-export-subscription.entity';
const INTERVAL_MONTHS: Record<RecurringTransactionIntervalEnum, number> = {
[RecurringTransactionIntervalEnum.monthly]: 1,
[RecurringTransactionIntervalEnum.quarterly]: 3,
[RecurringTransactionIntervalEnum.yearly]: 12,
};
@Injectable()
export class CashboxExportSubscriptionService {
constructor(
@InjectRepository(CashboxExportSubscription)
private readonly repository: Repository<CashboxExportSubscription>,
private readonly access: TeamAccessService,
) {}
async getSubscription(
teamId: number,
userId: number,
): Promise<CashboxExportSubscriptionResponseDTO> {
await this.assertAccess(userId, teamId);
const existing = await this.repository.findOne({ where: { team: { id: teamId } } });
if (!existing) {
return {
recipients: [],
interval: RecurringTransactionIntervalEnum.monthly,
active: false,
nextRunDate: null,
};
}
return this.toResponse(existing);
}
async upsertSubscription(
teamId: number,
userId: number,
dto: UpsertCashboxExportSubscriptionDTO,
): Promise<CashboxExportSubscriptionResponseDTO> {
await this.assertAccess(userId, teamId);
if (dto.active && dto.recipients.length === 0) {
throw new BadRequestException(
'Ein aktivierter automatischer Versand benötigt mindestens eine Empfängeradresse.',
);
}
const existing = await this.repository.findOne({ where: { team: { id: teamId } } });
const needsNewSchedule =
!existing || (dto.active && (!existing.active || existing.interval !== dto.interval));
const entity =
existing ??
this.repository.create({ team: { id: teamId } as any, nextRunDate: null, active: false });
entity.recipients = dto.recipients;
entity.interval = dto.interval;
entity.active = dto.active;
if (needsNewSchedule) {
entity.nextRunDate = this.nextBoundary(dto.interval);
}
const saved = await this.repository.save(entity);
return this.toResponse(saved);
}
private async assertAccess(userId: number, teamId: number): Promise<void> {
await this.access.assertAtLeast(
userId,
teamId,
'transaction_create_min_role',
TeamRolesEnum.scnd_treasurer,
);
}
private nextBoundary(interval: RecurringTransactionIntervalEnum): string {
const now = new Date();
const date = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
date.setUTCMonth(date.getUTCMonth() + INTERVAL_MONTHS[interval]);
return date.toISOString();
}
private toResponse(
entity: CashboxExportSubscription,
): CashboxExportSubscriptionResponseDTO {
return {
recipients: entity.recipients,
interval: entity.interval,
active: entity.active,
nextRunDate: entity.nextRunDate,
};
}
}