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
+107
-3
No files matched your search
@@ -8,8 +8,9 @@ import { CalendarSyncService } from '../calendar-sync/calendar-sync.service';
|
|||||||
describe('QueensService', () => {
|
describe('QueensService', () => {
|
||||||
let service: QueensService;
|
let service: QueensService;
|
||||||
let tx: {
|
let tx: {
|
||||||
|
queen: { update: jest.Mock };
|
||||||
queenColonyHistory: { updateMany: jest.Mock; create: jest.Mock };
|
queenColonyHistory: { updateMany: jest.Mock; create: jest.Mock };
|
||||||
colony: { update: jest.Mock };
|
colony: { findUnique: jest.Mock; update: jest.Mock };
|
||||||
};
|
};
|
||||||
let prisma: {
|
let prisma: {
|
||||||
queen: {
|
queen: {
|
||||||
@@ -30,8 +31,12 @@ describe('QueensService', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
tx = {
|
tx = {
|
||||||
|
queen: { update: jest.fn().mockResolvedValue(queen) },
|
||||||
queenColonyHistory: { updateMany: jest.fn(), create: jest.fn() },
|
queenColonyHistory: { updateMany: jest.fn(), create: jest.fn() },
|
||||||
colony: { update: jest.fn() },
|
colony: {
|
||||||
|
findUnique: jest.fn().mockResolvedValue(null),
|
||||||
|
update: jest.fn(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
prisma = {
|
prisma = {
|
||||||
queen: {
|
queen: {
|
||||||
@@ -163,6 +168,64 @@ describe('QueensService', () => {
|
|||||||
expect(prisma.queen.update).not.toHaveBeenCalled();
|
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 () => {
|
it('getLineage builds the ancestor chain and the descendant tree', async () => {
|
||||||
const grandmother = {
|
const grandmother = {
|
||||||
id: 'queen-gm',
|
id: 'queen-gm',
|
||||||
|
|||||||
@@ -40,6 +40,19 @@ export interface QueenLineage {
|
|||||||
|
|
||||||
const MAX_LINEAGE_DEPTH = 8;
|
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()
|
@Injectable()
|
||||||
export class QueensService {
|
export class QueensService {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -92,10 +105,38 @@ export class QueensService {
|
|||||||
}
|
}
|
||||||
await this.assertMotherQueenExists(dto.motherQueenId);
|
await this.assertMotherQueenExists(dto.motherQueenId);
|
||||||
}
|
}
|
||||||
const queen = await this.prisma.queen.update({
|
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 },
|
where: { id },
|
||||||
data: { ...dto },
|
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({
|
await this.auditLog.record({
|
||||||
entityType: 'Queen',
|
entityType: 'Queen',
|
||||||
entityId: id,
|
entityId: id,
|
||||||
|
|||||||
Reference in new issue
Block a user