diff --git a/ApiCore-Backend/src/colonies/colonies.service.spec.ts b/ApiCore-Backend/src/colonies/colonies.service.spec.ts index 5fe154a..09c74a3 100644 --- a/ApiCore-Backend/src/colonies/colonies.service.spec.ts +++ b/ApiCore-Backend/src/colonies/colonies.service.spec.ts @@ -11,6 +11,7 @@ describe('ColoniesService', () => { colony: { create: jest.Mock; update: jest.Mock; findUnique: jest.Mock }; colonyApiaryHistory: { create: jest.Mock }; colonyLineage: { create: jest.Mock }; + queenColonyHistory: { updateMany: jest.Mock }; }; let prisma: { colony: { @@ -39,6 +40,7 @@ describe('ColoniesService', () => { colony: { create: jest.fn(), update: jest.fn(), findUnique: jest.fn() }, colonyApiaryHistory: { create: jest.fn() }, colonyLineage: { create: jest.fn() }, + queenColonyHistory: { updateMany: jest.fn() }, }; prisma = { colony: { @@ -117,7 +119,7 @@ describe('ColoniesService', () => { expect(lineageArg.data.toColonyId).toBe('colony-2'); }); - it('merge marks the merged colony as MERGED and records a MERGE lineage entry', async () => { + it('merge marks the merged colony as MERGED, releases its queen, and records a MERGE lineage entry', async () => { prisma.colony.findUnique .mockResolvedValueOnce(parentColony) .mockResolvedValueOnce({ @@ -127,6 +129,7 @@ describe('ColoniesService', () => { tx.colony.findUnique.mockResolvedValue({ id: 'colony-3', status: 'ACTIVE', + currentQueenId: 'queen-2', }); tx.colony.update.mockResolvedValue({}); tx.colonyLineage.create.mockResolvedValue({}); @@ -138,10 +141,31 @@ describe('ColoniesService', () => { ); const [[updateArg]] = tx.colony.update.mock.calls as [ - [{ where: { id: string }; data: { status: string } }], + [ + { + where: { id: string }; + data: { status: string; currentQueenId: string | null }; + }, + ], ]; expect(updateArg.where.id).toBe('colony-3'); expect(updateArg.data.status).toBe('MERGED'); + expect(updateArg.data.currentQueenId).toBeNull(); + + const [[historyCloseArg]] = tx.queenColonyHistory.updateMany.mock.calls as [ + [ + { + where: { queenId: string; colonyId: string; endDate: null }; + data: { endDate: Date }; + }, + ], + ]; + expect(historyCloseArg.where).toEqual({ + queenId: 'queen-2', + colonyId: 'colony-3', + endDate: null, + }); + expect(historyCloseArg.data.endDate).toBeInstanceOf(Date); const [[lineageArg]] = tx.colonyLineage.create.mock.calls as [ [{ data: { type: string; fromColonyId: string; toColonyId: string } }], @@ -163,7 +187,7 @@ describe('ColoniesService', () => { it('dissolve sets status DISSOLVED and records DEACTIVATE', async () => { prisma.colony.findUnique.mockResolvedValue(parentColony); - prisma.colony.update.mockResolvedValue({ + tx.colony.update.mockResolvedValue({ ...parentColony, status: 'DISSOLVED', }); @@ -176,6 +200,67 @@ describe('ColoniesService', () => { ); }); + it("dissolve releases the colony's queen so she can be reassigned later", async () => { + prisma.colony.findUnique.mockResolvedValue({ + ...parentColony, + currentQueenId: 'queen-1', + }); + tx.colony.update.mockResolvedValue({ + ...parentColony, + status: 'DISSOLVED', + currentQueenId: null, + }); + + await service.dissolve('colony-1', 'user-1'); + + const [[dissolveUpdateArg]] = tx.colony.update.mock.calls as [ + [ + { + where: { id: string }; + data: { + status: string; + deactivatedAt: Date; + currentQueenId: string | null; + }; + }, + ], + ]; + expect(dissolveUpdateArg.where).toEqual({ id: 'colony-1' }); + expect(dissolveUpdateArg.data.status).toBe('DISSOLVED'); + expect(dissolveUpdateArg.data.currentQueenId).toBeNull(); + expect(dissolveUpdateArg.data.deactivatedAt).toBeInstanceOf(Date); + + const [[historyCloseArg]] = tx.queenColonyHistory.updateMany.mock.calls as [ + [ + { + where: { queenId: string; colonyId: string; endDate: null }; + data: { endDate: Date }; + }, + ], + ]; + expect(historyCloseArg.where).toEqual({ + queenId: 'queen-1', + colonyId: 'colony-1', + endDate: null, + }); + expect(historyCloseArg.data.endDate).toBeInstanceOf(Date); + }); + + it('dissolve does not touch queen history for a colony with no queen', async () => { + prisma.colony.findUnique.mockResolvedValue({ + ...parentColony, + currentQueenId: null, + }); + tx.colony.update.mockResolvedValue({ + ...parentColony, + status: 'DISSOLVED', + }); + + await service.dissolve('colony-1', 'user-1'); + + expect(tx.queenColonyHistory.updateMany).not.toHaveBeenCalled(); + }); + it('reactivate throws ConflictException when already active', async () => { prisma.colony.findUnique.mockResolvedValue(parentColony); await expect( diff --git a/ApiCore-Backend/src/colonies/colonies.service.ts b/ApiCore-Backend/src/colonies/colonies.service.ts index 77538c3..2d8fe8c 100644 --- a/ApiCore-Backend/src/colonies/colonies.service.ts +++ b/ApiCore-Backend/src/colonies/colonies.service.ts @@ -172,10 +172,30 @@ export class ColoniesService { throw new NotFoundException(`Colony ${mergedColonyId} not found`); } this.assertActive(merged); + // currentQueenId is unique across colonies - if it stayed set on + // this now-dead colony, any later attempt to assign her queen to a + // different colony would fail with a unique-constraint conflict. + // The survivor keeps its own queen; merged-away queens are simply + // released, not auto-transferred (which colony's queen "wins" a + // merge is a beekeeper decision, not one to guess here). await tx.colony.update({ where: { id: mergedColonyId }, - data: { status: 'MERGED', deactivatedAt: new Date() }, + data: { + status: 'MERGED', + deactivatedAt: new Date(), + currentQueenId: null, + }, }); + if (merged.currentQueenId) { + await tx.queenColonyHistory.updateMany({ + where: { + queenId: merged.currentQueenId, + colonyId: mergedColonyId, + endDate: null, + }, + data: { endDate: new Date() }, + }); + } await tx.colonyLineage.create({ data: { type: 'MERGE', @@ -200,9 +220,26 @@ export class ColoniesService { 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() }, + const colony = await this.prisma.$transaction(async (tx) => { + const updated = await tx.colony.update({ + where: { id }, + data: { + status: 'DISSOLVED', + deactivatedAt: new Date(), + currentQueenId: null, + }, + }); + if (before.currentQueenId) { + await tx.queenColonyHistory.updateMany({ + where: { + queenId: before.currentQueenId, + colonyId: id, + endDate: null, + }, + data: { endDate: new Date() }, + }); + } + return updated; }); await this.auditLog.record({ entityType: 'Colony',