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 { return this.prisma.colony.findMany({ where: { status: filter.status, apiaryId: filter.apiaryId }, orderBy: { code: 'asc' }, }); } async findById(id: string): Promise { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 }; } }