Files
ApiCore/ApiCore-Backend/src/colonies/colonies.service.ts
T
Nick Szaunig 6346bfab1d feat(backend): reactivate endpoint for dissolved colonies and inactive apiaries
Da fachliche Objekte nie hart geloescht werden (nur status +
deactivatedAt), fehlte bisher ein Weg, ein versehentlich aufgeloestes
Volk oder deaktivierten Bienenstand wieder zu aktivieren. Neue
POST :id/reactivate-Endpunkte setzen status zurueck auf ACTIVE und
loeschen deactivatedAt; ConflictException wenn bereits aktiv.
2026-08-23 11:37:06 +02:00

253 lines
7.0 KiB
TypeScript

import {
ConflictException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { AuditLogService } from '../audit/audit-log.service';
import { WebhooksService } from '../webhooks/webhooks.service';
import { Colony, ColonyStatus } from '../../generated/prisma';
import { CreateColonyDto } from './dto/create-colony.dto';
import { UpdateColonyDto } from './dto/update-colony.dto';
import { MoveColonyDto } from './dto/move-colony.dto';
import { SplitColonyDto } from './dto/split-colony.dto';
import { MergeColonyDto } from './dto/merge-colony.dto';
export interface ColonyFilter {
status?: ColonyStatus;
apiaryId?: string;
}
@Injectable()
export class ColoniesService {
constructor(
private readonly prisma: PrismaService,
private readonly auditLog: AuditLogService,
private readonly webhooks: WebhooksService,
) {}
async findAll(filter: ColonyFilter = {}): Promise<Colony[]> {
return this.prisma.colony.findMany({
where: { status: filter.status, apiaryId: filter.apiaryId },
orderBy: { code: 'asc' },
});
}
async findById(id: string): Promise<Colony> {
const colony = await this.prisma.colony.findUnique({ where: { id } });
if (!colony) {
throw new NotFoundException('Colony not found');
}
return colony;
}
private assertActive(colony: Colony): void {
if (colony.status !== 'ACTIVE') {
throw new ConflictException(
`Colony ${colony.id} is not active (status: ${colony.status})`,
);
}
}
async create(dto: CreateColonyDto, actingUserId: string): Promise<Colony> {
const colony = await this.prisma.colony.create({ data: { ...dto } });
await this.auditLog.record({
entityType: 'Colony',
entityId: colony.id,
action: 'CREATE',
userId: actingUserId,
afterData: colony,
});
await this.webhooks.dispatch('colony.created', colony);
return colony;
}
async update(
id: string,
dto: UpdateColonyDto,
actingUserId: string,
): Promise<Colony> {
const before = await this.findById(id);
const colony = await this.prisma.colony.update({
where: { id },
data: { ...dto },
});
await this.auditLog.record({
entityType: 'Colony',
entityId: id,
action: 'UPDATE',
userId: actingUserId,
beforeData: before,
afterData: colony,
});
return colony;
}
async move(
id: string,
dto: MoveColonyDto,
actingUserId: string,
): Promise<Colony> {
const before = await this.findById(id);
this.assertActive(before);
const colony = await this.prisma.$transaction(async (tx) => {
const updated = await tx.colony.update({
where: { id },
data: { apiaryId: dto.apiaryId },
});
await tx.colonyApiaryHistory.create({
data: {
colonyId: id,
apiaryId: dto.apiaryId,
movedByUserId: actingUserId,
notes: dto.notes,
},
});
return updated;
});
await this.auditLog.record({
entityType: 'Colony',
entityId: id,
action: 'UPDATE',
userId: actingUserId,
beforeData: before,
afterData: colony,
});
return colony;
}
async split(
id: string,
dto: SplitColonyDto,
actingUserId: string,
): Promise<Colony> {
const parent = await this.findById(id);
this.assertActive(parent);
const child = await this.prisma.$transaction(async (tx) => {
const created = await tx.colony.create({
data: {
code: dto.code,
apiaryId: parent.apiaryId,
colonyType: dto.colonyType,
notes: dto.notes,
},
});
await tx.colonyLineage.create({
data: {
type: 'SPLIT',
fromColonyId: parent.id,
toColonyId: created.id,
date: new Date(),
notes: dto.notes,
},
});
return created;
});
await this.auditLog.record({
entityType: 'Colony',
entityId: child.id,
action: 'CREATE',
userId: actingUserId,
afterData: { ...child, splitFromColonyId: parent.id },
});
return child;
}
async merge(
id: string,
dto: MergeColonyDto,
actingUserId: string,
): Promise<Colony> {
const survivor = await this.findById(id);
this.assertActive(survivor);
await this.prisma.$transaction(async (tx) => {
for (const mergedColonyId of dto.mergedColonyIds) {
const merged = await tx.colony.findUnique({
where: { id: mergedColonyId },
});
if (!merged) {
throw new NotFoundException(`Colony ${mergedColonyId} not found`);
}
this.assertActive(merged);
await tx.colony.update({
where: { id: mergedColonyId },
data: { status: 'MERGED', deactivatedAt: new Date() },
});
await tx.colonyLineage.create({
data: {
type: 'MERGE',
fromColonyId: mergedColonyId,
toColonyId: survivor.id,
date: new Date(),
notes: dto.notes,
},
});
}
});
await this.auditLog.record({
entityType: 'Colony',
entityId: survivor.id,
action: 'UPDATE',
userId: actingUserId,
beforeData: survivor,
afterData: { ...survivor, mergedColonyIds: dto.mergedColonyIds },
});
return this.findById(survivor.id);
}
async dissolve(id: string, actingUserId: string): Promise<Colony> {
const before = await this.findById(id);
const colony = await this.prisma.colony.update({
where: { id },
data: { status: 'DISSOLVED', deactivatedAt: new Date() },
});
await this.auditLog.record({
entityType: 'Colony',
entityId: id,
action: 'DEACTIVATE',
userId: actingUserId,
beforeData: before,
afterData: colony,
});
return colony;
}
async reactivate(id: string, actingUserId: string): Promise<Colony> {
const before = await this.findById(id);
if (before.status === 'ACTIVE') {
throw new ConflictException('Colony is already active');
}
const colony = await this.prisma.colony.update({
where: { id },
data: { status: 'ACTIVE', deactivatedAt: null },
});
await this.auditLog.record({
entityType: 'Colony',
entityId: id,
action: 'UPDATE',
userId: actingUserId,
beforeData: before,
afterData: colony,
});
return colony;
}
async getHistory(id: string) {
await this.findById(id);
const [apiaryHistory, queenHistory, lineageAsSource, lineageAsResult] =
await Promise.all([
this.prisma.colonyApiaryHistory.findMany({
where: { colonyId: id },
orderBy: { movedAt: 'desc' },
}),
this.prisma.queenColonyHistory.findMany({
where: { colonyId: id },
orderBy: { startDate: 'desc' },
}),
this.prisma.colonyLineage.findMany({ where: { fromColonyId: id } }),
this.prisma.colonyLineage.findMany({ where: { toColonyId: id } }),
]);
return { apiaryHistory, queenHistory, lineageAsSource, lineageAsResult };
}
}