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.
253 lines
7.0 KiB
TypeScript
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 };
|
|
}
|
|
}
|