Backend business-rule guards (inactive apiary blocking colony create/move, active colonies blocking apiary deactivation, negative quantity on IN/OUT stock movements, unavailable queen blocking colony assignment, inactive colony blocking split/merge/move) threw English, UUID-laden exception text that the frontend either discarded in favor of a generic message or - in colony-actions.tsx's move/split/merge/ assign-queen dialogs - didn't handle at all, so the guard fired with zero visible feedback. Translated the messages to plain German and wired the affected mutations to display them, so a beekeeper sees the actual reason a save was rejected instead of silence or "failed". Co-Authored-By: Claude Sonnet 5 <[email protected]>
127 lines
3.6 KiB
TypeScript
127 lines
3.6 KiB
TypeScript
import {
|
|
ConflictException,
|
|
Injectable,
|
|
NotFoundException,
|
|
} from '@nestjs/common';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
import { AuditLogService } from '../audit/audit-log.service';
|
|
import { Apiary, ApiaryStatus } from '../../generated/prisma';
|
|
import { CreateApiaryDto } from './dto/create-apiary.dto';
|
|
import { UpdateApiaryDto } from './dto/update-apiary.dto';
|
|
|
|
export interface ApiaryFilter {
|
|
status?: ApiaryStatus;
|
|
location?: string;
|
|
}
|
|
|
|
@Injectable()
|
|
export class ApiariesService {
|
|
constructor(
|
|
private readonly prisma: PrismaService,
|
|
private readonly auditLog: AuditLogService,
|
|
) {}
|
|
|
|
async findAll(filter: ApiaryFilter = {}): Promise<Apiary[]> {
|
|
return this.prisma.apiary.findMany({
|
|
where: {
|
|
status: filter.status,
|
|
location: filter.location
|
|
? { contains: filter.location, mode: 'insensitive' }
|
|
: undefined,
|
|
},
|
|
orderBy: { name: 'asc' },
|
|
});
|
|
}
|
|
|
|
async findById(id: string): Promise<Apiary> {
|
|
const apiary = await this.prisma.apiary.findUnique({ where: { id } });
|
|
if (!apiary) {
|
|
throw new NotFoundException('Apiary not found');
|
|
}
|
|
return apiary;
|
|
}
|
|
|
|
async create(dto: CreateApiaryDto, actingUserId: string): Promise<Apiary> {
|
|
const apiary = await this.prisma.apiary.create({ data: { ...dto } });
|
|
await this.auditLog.record({
|
|
entityType: 'Apiary',
|
|
entityId: apiary.id,
|
|
action: 'CREATE',
|
|
userId: actingUserId,
|
|
afterData: apiary,
|
|
});
|
|
return apiary;
|
|
}
|
|
|
|
async update(
|
|
id: string,
|
|
dto: UpdateApiaryDto,
|
|
actingUserId: string,
|
|
): Promise<Apiary> {
|
|
const before = await this.findById(id);
|
|
const apiary = await this.prisma.apiary.update({
|
|
where: { id },
|
|
data: { ...dto },
|
|
});
|
|
await this.auditLog.record({
|
|
entityType: 'Apiary',
|
|
entityId: id,
|
|
action: 'UPDATE',
|
|
userId: actingUserId,
|
|
beforeData: before,
|
|
afterData: apiary,
|
|
});
|
|
return apiary;
|
|
}
|
|
|
|
async deactivate(id: string, actingUserId: string): Promise<Apiary> {
|
|
const before = await this.findById(id);
|
|
// Colony.apiaryId is a plain FK, not enforced against apiary status -
|
|
// without this guard a colony can silently keep pointing at an
|
|
// inactive apiary (still shows ACTIVE, still countable, but its
|
|
// "location" has vanished from the active apiary list). Move the
|
|
// colonies elsewhere or dissolve them first instead.
|
|
const activeColonyCount = await this.prisma.colony.count({
|
|
where: { apiaryId: id, status: 'ACTIVE' },
|
|
});
|
|
if (activeColonyCount > 0) {
|
|
throw new ConflictException(
|
|
`Der Bienenstand hat noch ${activeColonyCount} aktive(s) Volk/Völker - zuerst verlegen oder auflösen`,
|
|
);
|
|
}
|
|
const apiary = await this.prisma.apiary.update({
|
|
where: { id },
|
|
data: { status: 'INACTIVE', deactivatedAt: new Date() },
|
|
});
|
|
await this.auditLog.record({
|
|
entityType: 'Apiary',
|
|
entityId: id,
|
|
action: 'DEACTIVATE',
|
|
userId: actingUserId,
|
|
beforeData: before,
|
|
afterData: apiary,
|
|
});
|
|
return apiary;
|
|
}
|
|
|
|
async reactivate(id: string, actingUserId: string): Promise<Apiary> {
|
|
const before = await this.findById(id);
|
|
if (before.status === 'ACTIVE') {
|
|
throw new ConflictException('Apiary is already active');
|
|
}
|
|
const apiary = await this.prisma.apiary.update({
|
|
where: { id },
|
|
data: { status: 'ACTIVE', deactivatedAt: null },
|
|
});
|
|
await this.auditLog.record({
|
|
entityType: 'Apiary',
|
|
entityId: id,
|
|
action: 'UPDATE',
|
|
userId: actingUserId,
|
|
beforeData: before,
|
|
afterData: apiary,
|
|
});
|
|
return apiary;
|
|
}
|
|
}
|