diff --git a/ApiCore-Backend/src/queens/queens.service.spec.ts b/ApiCore-Backend/src/queens/queens.service.spec.ts index 2c7f6eb..357f633 100644 --- a/ApiCore-Backend/src/queens/queens.service.spec.ts +++ b/ApiCore-Backend/src/queens/queens.service.spec.ts @@ -8,8 +8,9 @@ import { CalendarSyncService } from '../calendar-sync/calendar-sync.service'; describe('QueensService', () => { let service: QueensService; let tx: { + queen: { update: jest.Mock }; queenColonyHistory: { updateMany: jest.Mock; create: jest.Mock }; - colony: { update: jest.Mock }; + colony: { findUnique: jest.Mock; update: jest.Mock }; }; let prisma: { queen: { @@ -30,8 +31,12 @@ describe('QueensService', () => { beforeEach(() => { tx = { + queen: { update: jest.fn().mockResolvedValue(queen) }, queenColonyHistory: { updateMany: jest.fn(), create: jest.fn() }, - colony: { update: jest.fn() }, + colony: { + findUnique: jest.fn().mockResolvedValue(null), + update: jest.fn(), + }, }; prisma = { queen: { @@ -163,6 +168,64 @@ describe('QueensService', () => { expect(prisma.queen.update).not.toHaveBeenCalled(); }); + describe('when a queen becomes unavailable', () => { + it('clears currentQueenId and marks the colony QUEENLESS', async () => { + prisma.queen.findUnique.mockResolvedValue(queen); + tx.colony.findUnique.mockResolvedValue({ + id: 'colony-1', + status: 'ACTIVE', + }); + + await service.update('queen-1', { status: 'DECEASED' }, 'user-1'); + + expect(tx.colony.update).toHaveBeenCalledWith({ + where: { id: 'colony-1' }, + data: { currentQueenId: null, status: 'QUEENLESS' }, + }); + const [[closeArg]] = tx.queenColonyHistory.updateMany.mock.calls as [ + [{ where: { queenId: string; colonyId: string; endDate: null } }], + ]; + expect(closeArg.where).toEqual({ + queenId: 'queen-1', + colonyId: 'colony-1', + endDate: null, + }); + }); + + it('clears currentQueenId but leaves an already-terminal colony status alone', async () => { + prisma.queen.findUnique.mockResolvedValue(queen); + tx.colony.findUnique.mockResolvedValue({ + id: 'colony-1', + status: 'DISSOLVED', + }); + + await service.update('queen-1', { status: 'LOST' }, 'user-1'); + + expect(tx.colony.update).toHaveBeenCalledWith({ + where: { id: 'colony-1' }, + data: { currentQueenId: null, status: undefined }, + }); + }); + + it('does nothing when the queen is not currently assigned to a colony', async () => { + prisma.queen.findUnique.mockResolvedValue(queen); + tx.colony.findUnique.mockResolvedValue(null); + + await service.update('queen-1', { status: 'SOLD' }, 'user-1'); + + expect(tx.colony.update).not.toHaveBeenCalled(); + }); + + it('does not touch the colony for a status change that keeps the queen available', async () => { + prisma.queen.findUnique.mockResolvedValue(queen); + + await service.update('queen-1', { status: 'RESERVE' }, 'user-1'); + + expect(tx.colony.findUnique).not.toHaveBeenCalled(); + expect(tx.colony.update).not.toHaveBeenCalled(); + }); + }); + it('getLineage builds the ancestor chain and the descendant tree', async () => { const grandmother = { id: 'queen-gm', diff --git a/ApiCore-Backend/src/queens/queens.service.ts b/ApiCore-Backend/src/queens/queens.service.ts index 2a89967..1fe5c07 100644 --- a/ApiCore-Backend/src/queens/queens.service.ts +++ b/ApiCore-Backend/src/queens/queens.service.ts @@ -40,6 +40,19 @@ export interface QueenLineage { const MAX_LINEAGE_DEPTH = 8; +// Once a queen reaches one of these statuses she is no longer really "in" +// whatever colony currently points at her - a colony's currentQueenId and +// status would otherwise silently go stale (still showing a dead/lost +// queen as current, never flagged QUEENLESS) until someone happens to +// notice and fix it by hand. +const QUEEN_NO_LONGER_AVAILABLE_STATUSES: QueenStatus[] = [ + 'LOST', + 'DECEASED', + 'REPLACED', + 'SOLD', +]; +const TERMINAL_COLONY_STATUSES = ['DISSOLVED', 'SOLD', 'DECEASED', 'MERGED']; + @Injectable() export class QueensService { constructor( @@ -92,9 +105,37 @@ export class QueensService { } await this.assertMotherQueenExists(dto.motherQueenId); } - const queen = await this.prisma.queen.update({ - where: { id }, - data: { ...dto }, + const becameUnavailable = + dto.status !== undefined && + dto.status !== before.status && + QUEEN_NO_LONGER_AVAILABLE_STATUSES.includes(dto.status); + + const queen = await this.prisma.$transaction(async (tx) => { + const updated = await tx.queen.update({ + where: { id }, + data: { ...dto }, + }); + if (becameUnavailable) { + const colony = await tx.colony.findUnique({ + where: { currentQueenId: id }, + }); + if (colony) { + await tx.colony.update({ + where: { id: colony.id }, + data: { + currentQueenId: null, + status: TERMINAL_COLONY_STATUSES.includes(colony.status) + ? undefined + : 'QUEENLESS', + }, + }); + await tx.queenColonyHistory.updateMany({ + where: { queenId: id, colonyId: colony.id, endDate: null }, + data: { endDate: new Date() }, + }); + } + } + return updated; }); await this.auditLog.record({ entityType: 'Queen',