fix: mark a colony QUEENLESS when its queen is lost, dies, or is replaced
A very common real-world event - a queen dying over winter, absconding, being superseded, sold, or replaced - had no side effect on the colony record at all. Colony.currentQueenId kept pointing at a queen who was no longer really there, colony.status stayed whatever it was, and the QUEENLESS dashboard alert could never fire from this path. QueensService.update() now detects a transition into LOST/DECEASED/REPLACED/SOLD and, inside the same transaction, clears currentQueenId on whichever colony currently points at that queen, flips its status to QUEENLESS (unless the colony is already in a terminal lifecycle status like DISSOLVED, which is left alone), and closes the open QueenColonyHistory row - mirroring the bookkeeping assignColony() already does when a queen changes colonies. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
32f11e5b39
commit
cc46fa4d9e
2 files changed
+109
-5
No files matched your search
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in new issue
Block a user