fix(queens): verlorene, eingegangene, ersetzte und verkaufte Königinnen ins Archiv
Die Königinnen-Liste trennt Aktiv und Historie über deactivatedAt, das Backend setzte das Feld aber nie. Jetzt stempelt ein Statuswechsel auf LOST, DECEASED, REPLACED oder SOLD das Datum (update und updateStatusIn), ein korrigierter Status nimmt es wieder weg. Die Migration 20261021100000_queen_deactivated_backfill trägt das Datum für bestehende Königinnen nach (Ende der letzten Volkszuordnung, sonst updatedAt). Auch die Lebensdauer in der Statistik rechnet damit wieder. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
39eee3cdb1
commit
1688e77a89
5 files changed
+227
-3
No files matched your search
@@ -305,6 +305,66 @@ describe('QueensService', () => {
|
||||
expect(tx.colony.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('stamps deactivatedAt, which moves her into the history', async () => {
|
||||
prisma.queen.findUnique.mockResolvedValue({
|
||||
...queen,
|
||||
deactivatedAt: null,
|
||||
});
|
||||
|
||||
await service.update('queen-1', { status: 'DECEASED' }, 'user-1');
|
||||
|
||||
expect(tx.queen.update).toHaveBeenCalledWith({
|
||||
where: { id: 'queen-1' },
|
||||
data: { status: 'DECEASED', deactivatedAt: expect.any(Date) as Date },
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the first deactivatedAt when she moves between unavailable statuses', async () => {
|
||||
const diedOn = new Date('2026-05-01T00:00:00Z');
|
||||
prisma.queen.findUnique.mockResolvedValue({
|
||||
...queen,
|
||||
status: 'DECEASED',
|
||||
deactivatedAt: diedOn,
|
||||
});
|
||||
|
||||
await service.update('queen-1', { status: 'LOST' }, 'user-1');
|
||||
|
||||
expect(tx.queen.update).toHaveBeenCalledWith({
|
||||
where: { id: 'queen-1' },
|
||||
data: { status: 'LOST', deactivatedAt: diedOn },
|
||||
});
|
||||
});
|
||||
|
||||
it('clears deactivatedAt when the status is corrected back to an available one', async () => {
|
||||
prisma.queen.findUnique.mockResolvedValue({
|
||||
...queen,
|
||||
status: 'DECEASED',
|
||||
deactivatedAt: new Date('2026-05-01T00:00:00Z'),
|
||||
});
|
||||
|
||||
await service.update('queen-1', { status: 'ACTIVE' }, 'user-1');
|
||||
|
||||
expect(tx.queen.update).toHaveBeenCalledWith({
|
||||
where: { id: 'queen-1' },
|
||||
data: { status: 'ACTIVE', deactivatedAt: null },
|
||||
});
|
||||
});
|
||||
|
||||
it('leaves deactivatedAt alone when the status is not part of the change', async () => {
|
||||
prisma.queen.findUnique.mockResolvedValue({
|
||||
...queen,
|
||||
status: 'DECEASED',
|
||||
deactivatedAt: new Date('2026-05-01T00:00:00Z'),
|
||||
});
|
||||
|
||||
await service.update('queen-1', { notes: 'Notiz' }, 'user-1');
|
||||
|
||||
expect(tx.queen.update).toHaveBeenCalledWith({
|
||||
where: { id: 'queen-1' },
|
||||
data: { notes: 'Notiz', deactivatedAt: undefined },
|
||||
});
|
||||
});
|
||||
|
||||
it('does not touch the colony for a status change that keeps the queen available', async () => {
|
||||
prisma.queen.findUnique.mockResolvedValue(queen);
|
||||
|
||||
@@ -392,7 +452,7 @@ describe('QueensService', () => {
|
||||
);
|
||||
expect(tx.queen.update).toHaveBeenCalledWith({
|
||||
where: { id: 'queen-1' },
|
||||
data: { status: 'DECEASED' },
|
||||
data: { status: 'DECEASED', deactivatedAt: expect.any(Date) as Date },
|
||||
});
|
||||
expect(auditLog.record).toHaveBeenCalledWith(
|
||||
{
|
||||
@@ -408,6 +468,21 @@ describe('QueensService', () => {
|
||||
expect(prisma.$transaction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('clears deactivatedAt when a status is corrected back to an available one', async () => {
|
||||
tx.queen.findUnique.mockResolvedValue({
|
||||
...queen,
|
||||
status: 'SOLD',
|
||||
deactivatedAt: new Date('2026-05-01T00:00:00Z'),
|
||||
});
|
||||
|
||||
await service.updateStatusIn(tx as never, 'queen-1', 'RESERVE', 'user-1');
|
||||
|
||||
expect(tx.queen.update).toHaveBeenCalledWith({
|
||||
where: { id: 'queen-1' },
|
||||
data: { status: 'RESERVE', deactivatedAt: null },
|
||||
});
|
||||
});
|
||||
|
||||
it('answers 404 for an unknown queen', async () => {
|
||||
tx.queen.findUnique.mockResolvedValue(null);
|
||||
|
||||
|
||||
@@ -61,6 +61,19 @@ const QUEEN_NO_LONGER_AVAILABLE_STATUSES: QueenStatus[] = [
|
||||
'SOLD',
|
||||
];
|
||||
|
||||
/**
|
||||
* deactivatedAt for a queen moving to `status`: the /queens history and the
|
||||
* queen lifespans in the statistics read it. Stamped when she becomes
|
||||
* unavailable (the first date kept on a move between such statuses), cleared
|
||||
* when a status is corrected back to an available one.
|
||||
*/
|
||||
function deactivatedAtFor(before: Queen, status: QueenStatus): Date | null {
|
||||
if (QUEEN_NO_LONGER_AVAILABLE_STATUSES.includes(status)) {
|
||||
return before.deactivatedAt ?? new Date();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class QueensService {
|
||||
constructor(
|
||||
@@ -121,7 +134,13 @@ export class QueensService {
|
||||
const queen = await this.prisma.$transaction(async (tx) => {
|
||||
const updated = await tx.queen.update({
|
||||
where: { id },
|
||||
data: { ...dto },
|
||||
data: {
|
||||
...dto,
|
||||
deactivatedAt:
|
||||
dto.status === undefined
|
||||
? undefined
|
||||
: deactivatedAtFor(before, dto.status),
|
||||
},
|
||||
});
|
||||
if (becameUnavailable) {
|
||||
const colony = await tx.colony.findUnique({
|
||||
@@ -195,7 +214,10 @@ export class QueensService {
|
||||
);
|
||||
}
|
||||
}
|
||||
const updated = await tx.queen.update({ where: { id }, data: { status } });
|
||||
const updated = await tx.queen.update({
|
||||
where: { id },
|
||||
data: { status, deactivatedAt: deactivatedAtFor(before, status) },
|
||||
});
|
||||
await this.auditLog.record(
|
||||
{
|
||||
entityType: 'Queen',
|
||||
|
||||
Reference in new issue
Block a user