feat: T4 wave 5 - colony end cleanup, withdrawal chain, next steps, inspection takeover UI
- Colony end releases the queen in the same transaction; open suggestions become obsolete and a CLEANUP suggestion lists open tasks and the queen; apply-cleanup validates evidence and blocks reactivated colonies - Withdrawal chain: plausibility warnings on harvest and bottling, conflict notifications also when a treatment is entered or corrected after the harvest (naming bottled lots), withdrawal spans in the calendar and M365 sync, Berlin-day end events - Next 3 steps per colony and the "Heute" endpoint with ranking and an explicit merge table by intent - Inspection-triggered rule suggestions follow the colony's latest inspection (resolve when the finding is gone, refresh when it stays) - Frontend: colony card takeover in the inspection form, feed status as condition, occupied frames 0-40; legacy offline entries keep syncing - Notifications and preferences are deleted with their user Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
02eb16924e
commit
ef33e5549b
122 files changed
+10707
-504
No files matched your search
@@ -0,0 +1,586 @@
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import request from 'supertest';
|
||||
import { App } from 'supertest/types';
|
||||
import { PrismaService } from '../src/prisma/prisma.service';
|
||||
import { login } from './utils/api';
|
||||
import { createTestApp } from './utils/create-test-app';
|
||||
import { drainDomainEvents } from './utils/domain-events';
|
||||
import { proposeForTest } from './utils/suggestions';
|
||||
|
||||
interface CleanupBody {
|
||||
cancelled: number;
|
||||
completed: number;
|
||||
kept: number;
|
||||
queenStatus: string | null;
|
||||
}
|
||||
|
||||
interface EndedColony {
|
||||
colonyId: string;
|
||||
queenId: string;
|
||||
taskIds: string[];
|
||||
suggestionId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A colony that ends (winter loss, edit to SOLD): the queen assignment ends
|
||||
* in the same transaction, open suggestions turn OBSOLETE, one clean-up
|
||||
* suggestion is left, and "Aufräumen" settles tasks and queen. Every
|
||||
* assertion is on this file's own colonies.
|
||||
*/
|
||||
describe('Colony ended (e2e)', () => {
|
||||
let app: INestApplication<App>;
|
||||
let prisma: PrismaService;
|
||||
const suffix = `${Date.now()}-${Math.round(Math.random() * 1e6)}`;
|
||||
const password = 'SuperSecret123!';
|
||||
const FULL_PERMISSIONS = [
|
||||
'tasks:read',
|
||||
'tasks:create',
|
||||
'tasks:update',
|
||||
'tasks:complete',
|
||||
'queens:update',
|
||||
'colonies:read',
|
||||
'colonies:update',
|
||||
'colonies:deactivate',
|
||||
'winter-checks:read',
|
||||
'winter-checks:create',
|
||||
];
|
||||
let apiaryId: string;
|
||||
const colonyIds: string[] = [];
|
||||
const queenIds: string[] = [];
|
||||
const roleIds: string[] = [];
|
||||
const userIds: string[] = [];
|
||||
const tokens = { full: '', noComplete: '', noQueens: '' };
|
||||
let counter = 0;
|
||||
const nextKey = () => `colony-ended-${suffix}-${(counter += 1)}-key`;
|
||||
|
||||
const api = () => request(app.getHttpServer());
|
||||
const auth = (token: string) => ({ Authorization: `Bearer ${token}` });
|
||||
|
||||
async function createUser(
|
||||
label: string,
|
||||
permissions: string[],
|
||||
): Promise<string> {
|
||||
const role = await prisma.role.create({
|
||||
data: {
|
||||
key: `COLONY_ENDED_${label.toUpperCase()}_${suffix}`,
|
||||
name: `Colony ended ${label}`,
|
||||
permissions: {
|
||||
create: permissions.map((key) => ({
|
||||
permission: { connect: { key } },
|
||||
})),
|
||||
},
|
||||
},
|
||||
});
|
||||
roleIds.push(role.id);
|
||||
const email = `colony-ended-${label}-${suffix}@example.com`;
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
email,
|
||||
passwordHash: await bcrypt.hash(password, 12),
|
||||
name: `Colony ended ${label}`,
|
||||
role: { connect: { id: role.id } },
|
||||
},
|
||||
});
|
||||
userIds.push(user.id);
|
||||
return (await login(app, email, password)).accessToken;
|
||||
}
|
||||
|
||||
/** A living colony with an active queen assigned today and some open tasks. */
|
||||
async function newColony(
|
||||
label: string,
|
||||
taskCount = 3,
|
||||
): Promise<{ colonyId: string; queenId: string; taskIds: string[] }> {
|
||||
const queen = await prisma.queen.create({
|
||||
data: { code: `CE-Q-${label}-${suffix}`, status: 'ACTIVE' },
|
||||
});
|
||||
queenIds.push(queen.id);
|
||||
const colony = await prisma.colony.create({
|
||||
data: {
|
||||
code: `CE-${label}-${suffix}`,
|
||||
apiaryId,
|
||||
colonyType: 'PRODUCTION_COLONY',
|
||||
currentQueenId: queen.id,
|
||||
},
|
||||
});
|
||||
colonyIds.push(colony.id);
|
||||
await prisma.queenColonyHistory.create({
|
||||
data: {
|
||||
queenId: queen.id,
|
||||
colonyId: colony.id,
|
||||
startDate: new Date('2002-04-01T00:00:00.000Z'),
|
||||
},
|
||||
});
|
||||
const taskIds: string[] = [];
|
||||
for (let index = 0; index < taskCount; index += 1) {
|
||||
const task = await prisma.task.create({
|
||||
data: {
|
||||
title: `Aufgabe ${index}`,
|
||||
relatedType: 'COLONY',
|
||||
relatedId: colony.id,
|
||||
},
|
||||
});
|
||||
taskIds.push(task.id);
|
||||
}
|
||||
return { colonyId: colony.id, queenId: queen.id, taskIds };
|
||||
}
|
||||
|
||||
/** Ends a colony through the edit route and returns its clean-up suggestion. */
|
||||
async function endColony(label: string): Promise<EndedColony> {
|
||||
const colony = await newColony(label);
|
||||
await api()
|
||||
.patch(`/api/v1/colonies/${colony.colonyId}`)
|
||||
.set(auth(tokens.full))
|
||||
.send({ status: 'SOLD' })
|
||||
.expect(200);
|
||||
await drainDomainEvents(app, [colony.colonyId]);
|
||||
const suggestion = await prisma.suggestion.findFirstOrThrow({
|
||||
where: { colonyId: colony.colonyId, intent: 'CLEANUP', status: 'OPEN' },
|
||||
});
|
||||
return { ...colony, suggestionId: suggestion.id };
|
||||
}
|
||||
|
||||
const applyCleanup = (token: string, suggestionId: string, body: object) =>
|
||||
api()
|
||||
.post(`/api/v1/suggestions/${suggestionId}/apply-cleanup`)
|
||||
.set(auth(token))
|
||||
.set('Idempotency-Key', nextKey())
|
||||
.send(body);
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await createTestApp();
|
||||
prisma = app.get(PrismaService);
|
||||
apiaryId = (
|
||||
await prisma.apiary.create({ data: { name: `Colony ended ${suffix}` } })
|
||||
).id;
|
||||
tokens.full = await createUser('full', FULL_PERMISSIONS);
|
||||
tokens.noQueens = await createUser(
|
||||
'noqueens',
|
||||
FULL_PERMISSIONS.filter((key) => key !== 'queens:update'),
|
||||
);
|
||||
tokens.noComplete = await createUser(
|
||||
'nocomplete',
|
||||
FULL_PERMISSIONS.filter((key) => key !== 'tasks:complete'),
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const taskIds = (
|
||||
await prisma.task.findMany({
|
||||
where: { relatedId: { in: colonyIds } },
|
||||
select: { id: true },
|
||||
})
|
||||
).map((row) => row.id);
|
||||
await prisma.domainEvent.deleteMany({
|
||||
where: { aggregateId: { in: [...taskIds, ...colonyIds, ...queenIds] } },
|
||||
});
|
||||
await prisma.calendarSyncOutboxEntry.deleteMany({
|
||||
where: { sourceType: 'TASK', sourceId: { in: taskIds } },
|
||||
});
|
||||
await prisma.task.deleteMany({ where: { id: { in: taskIds } } });
|
||||
await prisma.suggestion.deleteMany({
|
||||
where: { colonyId: { in: colonyIds } },
|
||||
});
|
||||
await prisma.winterCheck.deleteMany({
|
||||
where: { colonyId: { in: colonyIds } },
|
||||
});
|
||||
await prisma.queenColonyHistory.deleteMany({
|
||||
where: { colonyId: { in: colonyIds } },
|
||||
});
|
||||
await prisma.colony.deleteMany({ where: { id: { in: colonyIds } } });
|
||||
await prisma.queen.deleteMany({ where: { id: { in: queenIds } } });
|
||||
await prisma.apiary.delete({ where: { id: apiaryId } });
|
||||
await prisma.idempotencyRecord.deleteMany({
|
||||
where: { userId: { in: userIds } },
|
||||
});
|
||||
await prisma.refreshToken.deleteMany({
|
||||
where: { userId: { in: userIds } },
|
||||
});
|
||||
await prisma.auditLog.deleteMany({ where: { userId: { in: userIds } } });
|
||||
await prisma.user.deleteMany({ where: { id: { in: userIds } } });
|
||||
await prisma.role.deleteMany({ where: { id: { in: roleIds } } });
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it('a winter loss with markColonyDeceased ends the queen assignment in the same transaction', async () => {
|
||||
const { colonyId, queenId } = await newColony('winter', 0);
|
||||
|
||||
await api()
|
||||
.post('/api/v1/winter-checks')
|
||||
.set(auth(tokens.full))
|
||||
.send({
|
||||
colonyId,
|
||||
season: 'SPRING',
|
||||
date: '2002-03-20',
|
||||
outcome: 'LOST',
|
||||
lossCause: 'DEAD_OR_EMPTY',
|
||||
markColonyDeceased: true,
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
const colony = await prisma.colony.findUniqueOrThrow({
|
||||
where: { id: colonyId },
|
||||
});
|
||||
expect(colony).toMatchObject({ status: 'DECEASED', currentQueenId: null });
|
||||
const history = await prisma.queenColonyHistory.findMany({
|
||||
where: { colonyId, queenId },
|
||||
});
|
||||
expect(history).toHaveLength(1);
|
||||
expect(history[0].endDate?.toISOString()).toBe('2002-03-20T00:00:00.000Z');
|
||||
// The queen herself keeps her status: the clean-up decides.
|
||||
await expect(
|
||||
prisma.queen.findUniqueOrThrow({ where: { id: queenId } }),
|
||||
).resolves.toMatchObject({ status: 'ACTIVE' });
|
||||
const ended = await prisma.domainEvent.findMany({
|
||||
where: { aggregateId: colonyId, type: 'ColonyEnded' },
|
||||
});
|
||||
expect(ended.map((event) => event.payload)).toEqual([
|
||||
{
|
||||
status: 'DECEASED',
|
||||
endedOn: '2002-03-20',
|
||||
releasedQueenId: queenId,
|
||||
via: 'WINTER_CHECK',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('open suggestions become OBSOLETE after draining and one CLEANUP suggestion is left', async () => {
|
||||
const { colonyId, taskIds, queenId } = await newColony('obsolete', 2);
|
||||
const open = await proposeForTest(app, {
|
||||
ruleKey: 'INSPECTION_INTERVAL',
|
||||
source: 'RULE',
|
||||
mode: 'DO',
|
||||
intent: 'INSPECT',
|
||||
relatedType: 'COLONY',
|
||||
relatedId: colonyId,
|
||||
colonyId,
|
||||
reasonCode: `COLONY_ENDED_E2E_${suffix}`,
|
||||
evidence: { colonyId },
|
||||
requiredPermission: 'inspections:read',
|
||||
dueDate: '2026-05-12',
|
||||
anchor: `ce-${suffix}`,
|
||||
});
|
||||
|
||||
await api()
|
||||
.patch(`/api/v1/colonies/${colonyId}`)
|
||||
.set(auth(tokens.full))
|
||||
.send({ status: 'DECEASED' })
|
||||
.expect(200);
|
||||
await drainDomainEvents(app, [colonyId]);
|
||||
|
||||
await expect(
|
||||
prisma.suggestion.findUniqueOrThrow({ where: { id: open.id } }),
|
||||
).resolves.toMatchObject({ status: 'OBSOLETE' });
|
||||
const cleanups = await prisma.suggestion.findMany({
|
||||
where: { colonyId, intent: 'CLEANUP' },
|
||||
});
|
||||
expect(cleanups).toHaveLength(1);
|
||||
expect(cleanups[0]).toMatchObject({
|
||||
status: 'OPEN',
|
||||
mode: 'DO',
|
||||
ruleKey: 'COLONY_ENDED_CLEANUP',
|
||||
requiredPermission: 'colonies:read',
|
||||
});
|
||||
const evidence = cleanups[0].evidence as Record<string, unknown>;
|
||||
expect(evidence).toMatchObject({
|
||||
colonyId,
|
||||
status: 'DECEASED',
|
||||
via: 'EDIT',
|
||||
queenId,
|
||||
});
|
||||
expect(evidence.openTaskIds).toEqual([...taskIds].sort());
|
||||
const colony = await prisma.colony.findUniqueOrThrow({
|
||||
where: { id: colonyId },
|
||||
});
|
||||
expect(colony.currentQueenId).toBeNull();
|
||||
});
|
||||
|
||||
it('apply-cleanup cancels 2 tasks and completes 1, then the suggestion is RESOLVED', async () => {
|
||||
const ended = await endColony('apply');
|
||||
const [first, second, third] = [...ended.taskIds].sort();
|
||||
|
||||
const response = await applyCleanup(tokens.full, ended.suggestionId, {
|
||||
tasks: [
|
||||
{ id: first, outcome: 'CANCEL' },
|
||||
{ id: second, outcome: 'CANCEL' },
|
||||
{ id: third, outcome: 'DONE' },
|
||||
],
|
||||
queen: { id: ended.queenId, status: 'SOLD' },
|
||||
}).expect(201);
|
||||
|
||||
expect(response.body as CleanupBody).toEqual({
|
||||
cancelled: 2,
|
||||
completed: 1,
|
||||
kept: 0,
|
||||
queenStatus: 'SOLD',
|
||||
});
|
||||
const tasks = await prisma.task.findMany({
|
||||
where: { id: { in: ended.taskIds } },
|
||||
orderBy: { id: 'asc' },
|
||||
});
|
||||
expect(tasks.map((task) => task.status)).toEqual([
|
||||
'CANCELLED',
|
||||
'CANCELLED',
|
||||
'DONE',
|
||||
]);
|
||||
expect(tasks[2].completedAt).not.toBeNull();
|
||||
await expect(
|
||||
prisma.queen.findUniqueOrThrow({ where: { id: ended.queenId } }),
|
||||
).resolves.toMatchObject({ status: 'SOLD' });
|
||||
await drainDomainEvents(app, [first, second]);
|
||||
for (const id of [first, second]) {
|
||||
const updates = await prisma.domainEvent.findMany({
|
||||
where: { aggregateId: id, type: 'TaskUpdated' },
|
||||
include: { deliveries: true },
|
||||
});
|
||||
expect(updates).toHaveLength(1);
|
||||
expect(updates[0].payload).toMatchObject({ status: 'CANCELLED' });
|
||||
expect(updates[0].deliveries).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
handler: 'calendar-sync.task',
|
||||
status: 'DONE',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
}
|
||||
const suggestion = await prisma.suggestion.findUniqueOrThrow({
|
||||
where: { id: ended.suggestionId },
|
||||
});
|
||||
expect(suggestion).toMatchObject({
|
||||
status: 'RESOLVED',
|
||||
decisionNote: 'Aufgeräumt',
|
||||
dedupeKey: null,
|
||||
});
|
||||
await expect(
|
||||
prisma.auditLog.count({
|
||||
where: {
|
||||
entityType: 'Suggestion',
|
||||
entityId: ended.suggestionId,
|
||||
action: 'UPDATE',
|
||||
},
|
||||
}),
|
||||
).resolves.toBe(1);
|
||||
});
|
||||
|
||||
it('apply-cleanup DONE writes the audit entry and TaskCompleted like a normal completion (delivered after draining)', async () => {
|
||||
const ended = await endColony('done');
|
||||
const [first, ...rest] = [...ended.taskIds].sort();
|
||||
|
||||
await applyCleanup(tokens.full, ended.suggestionId, {
|
||||
tasks: [
|
||||
{ id: first, outcome: 'DONE' },
|
||||
...rest.map((id) => ({ id, outcome: 'KEEP' })),
|
||||
],
|
||||
}).expect(201);
|
||||
await drainDomainEvents(app, [first]);
|
||||
|
||||
const audit = await prisma.auditLog.findMany({
|
||||
where: { entityType: 'Task', entityId: first, action: 'UPDATE' },
|
||||
});
|
||||
expect(audit).toHaveLength(1);
|
||||
expect(audit[0].afterData).toMatchObject({ status: 'DONE' });
|
||||
const completed = await prisma.domainEvent.findMany({
|
||||
where: { aggregateId: first, type: 'TaskCompleted' },
|
||||
include: { deliveries: true },
|
||||
});
|
||||
expect(completed).toHaveLength(1);
|
||||
expect(completed[0].deliveries).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
handler: 'calendar-sync.task',
|
||||
status: 'DONE',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
const kept = await prisma.task.findMany({
|
||||
where: { id: { in: rest } },
|
||||
});
|
||||
expect(kept.map((task) => task.status)).toEqual(['OPEN', 'OPEN']);
|
||||
});
|
||||
|
||||
it('403 without tasks:complete when one task is DONE, and nothing changes', async () => {
|
||||
const ended = await endColony('forbidden');
|
||||
const [first] = [...ended.taskIds].sort();
|
||||
|
||||
await applyCleanup(tokens.noComplete, ended.suggestionId, {
|
||||
tasks: [{ id: first, outcome: 'DONE' }],
|
||||
}).expect(403);
|
||||
|
||||
await expect(
|
||||
prisma.task.findUniqueOrThrow({ where: { id: first } }),
|
||||
).resolves.toMatchObject({ status: 'OPEN' });
|
||||
await expect(
|
||||
prisma.suggestion.findUniqueOrThrow({
|
||||
where: { id: ended.suggestionId },
|
||||
}),
|
||||
).resolves.toMatchObject({ status: 'OPEN' });
|
||||
});
|
||||
|
||||
it('400 for a task id that is not in the evidence', async () => {
|
||||
const ended = await endColony('foreign');
|
||||
const other = await newColony('foreign-other', 1);
|
||||
|
||||
const response = await applyCleanup(tokens.full, ended.suggestionId, {
|
||||
tasks: [{ id: other.taskIds[0], outcome: 'CANCEL' }],
|
||||
}).expect(400);
|
||||
|
||||
expect((response.body as { message: string }).message).toBe(
|
||||
'Nur Aufgaben und Königin aus dem Vorschlag.',
|
||||
);
|
||||
await expect(
|
||||
prisma.task.findUniqueOrThrow({ where: { id: other.taskIds[0] } }),
|
||||
).resolves.toMatchObject({ status: 'OPEN' });
|
||||
});
|
||||
|
||||
it('409 when the suggestion was cleaned up already', async () => {
|
||||
const ended = await endColony('twice');
|
||||
|
||||
await applyCleanup(tokens.full, ended.suggestionId, {
|
||||
tasks: [],
|
||||
}).expect(201);
|
||||
const second = await applyCleanup(tokens.full, ended.suggestionId, {
|
||||
tasks: [],
|
||||
}).expect(409);
|
||||
|
||||
expect(second.body).toMatchObject({
|
||||
message: 'Der Vorschlag ist nicht mehr offen.',
|
||||
status: 'RESOLVED',
|
||||
});
|
||||
});
|
||||
|
||||
it('403 without queens:update when the queen is given, and nothing changes', async () => {
|
||||
const ended = await endColony('noqueens');
|
||||
|
||||
await applyCleanup(tokens.noQueens, ended.suggestionId, {
|
||||
tasks: [],
|
||||
queen: { id: ended.queenId, status: 'SOLD' },
|
||||
}).expect(403);
|
||||
|
||||
await expect(
|
||||
prisma.queen.findUniqueOrThrow({ where: { id: ended.queenId } }),
|
||||
).resolves.toMatchObject({ status: 'ACTIVE' });
|
||||
});
|
||||
|
||||
it('400 for a queen id that is not in the evidence', async () => {
|
||||
const ended = await endColony('foreignqueen');
|
||||
const other = await newColony('foreignqueen-other', 0);
|
||||
|
||||
await applyCleanup(tokens.full, ended.suggestionId, {
|
||||
tasks: [],
|
||||
queen: { id: other.queenId, status: 'SOLD' },
|
||||
}).expect(400);
|
||||
|
||||
await expect(
|
||||
prisma.queen.findUniqueOrThrow({ where: { id: other.queenId } }),
|
||||
).resolves.toMatchObject({ status: 'ACTIVE' });
|
||||
});
|
||||
|
||||
it('400 when the evidence has no queen but the request names one', async () => {
|
||||
const colony = await newColony('noqueenevidence');
|
||||
await prisma.queen.update({
|
||||
where: { id: colony.queenId },
|
||||
data: { status: 'LOST' },
|
||||
});
|
||||
await api()
|
||||
.patch(`/api/v1/colonies/${colony.colonyId}`)
|
||||
.set(auth(tokens.full))
|
||||
.send({ status: 'SOLD' })
|
||||
.expect(200);
|
||||
await drainDomainEvents(app, [colony.colonyId]);
|
||||
const suggestion = await prisma.suggestion.findFirstOrThrow({
|
||||
where: { colonyId: colony.colonyId, intent: 'CLEANUP', status: 'OPEN' },
|
||||
});
|
||||
expect((suggestion.evidence as { queenId: unknown }).queenId).toBeNull();
|
||||
|
||||
await applyCleanup(tokens.full, suggestion.id, {
|
||||
tasks: [],
|
||||
queen: { id: colony.queenId, status: 'SOLD' },
|
||||
}).expect(400);
|
||||
});
|
||||
|
||||
it('409 when a colony holds the queen again, and the whole clean-up rolls back', async () => {
|
||||
const ended = await endColony('held');
|
||||
const holder = await newColony('held-holder', 0);
|
||||
await prisma.colony.update({
|
||||
where: { id: holder.colonyId },
|
||||
data: { currentQueenId: ended.queenId },
|
||||
});
|
||||
const [first] = [...ended.taskIds].sort();
|
||||
|
||||
await applyCleanup(tokens.full, ended.suggestionId, {
|
||||
tasks: [{ id: first, outcome: 'CANCEL' }],
|
||||
queen: { id: ended.queenId, status: 'DECEASED' },
|
||||
}).expect(409);
|
||||
|
||||
await expect(
|
||||
prisma.task.findUniqueOrThrow({ where: { id: first } }),
|
||||
).resolves.toMatchObject({ status: 'OPEN' });
|
||||
await expect(
|
||||
prisma.suggestion.findUniqueOrThrow({
|
||||
where: { id: ended.suggestionId },
|
||||
}),
|
||||
).resolves.toMatchObject({ status: 'OPEN' });
|
||||
await expect(
|
||||
prisma.queen.findUniqueOrThrow({ where: { id: ended.queenId } }),
|
||||
).resolves.toMatchObject({ status: 'ACTIVE' });
|
||||
});
|
||||
|
||||
it('reactivating the colony turns its open CLEANUP suggestion OBSOLETE', async () => {
|
||||
const ended = await endColony('reactivate');
|
||||
|
||||
await api()
|
||||
.post(`/api/v1/colonies/${ended.colonyId}/reactivate`)
|
||||
.set(auth(tokens.full))
|
||||
.expect(201);
|
||||
|
||||
await expect(
|
||||
prisma.suggestion.findUniqueOrThrow({
|
||||
where: { id: ended.suggestionId },
|
||||
}),
|
||||
).resolves.toMatchObject({ status: 'OBSOLETE' });
|
||||
});
|
||||
|
||||
it('409 "Das Volk ist wieder aktiv." when the colony lives again but the suggestion is still open', async () => {
|
||||
const ended = await endColony('living');
|
||||
await prisma.colony.update({
|
||||
where: { id: ended.colonyId },
|
||||
data: { status: 'ACTIVE', deactivatedAt: null },
|
||||
});
|
||||
const [first] = [...ended.taskIds].sort();
|
||||
|
||||
const response = await applyCleanup(tokens.full, ended.suggestionId, {
|
||||
tasks: [{ id: first, outcome: 'CANCEL' }],
|
||||
}).expect(409);
|
||||
|
||||
expect((response.body as { message: string }).message).toBe(
|
||||
'Das Volk ist wieder aktiv.',
|
||||
);
|
||||
await expect(
|
||||
prisma.task.findUniqueOrThrow({ where: { id: first } }),
|
||||
).resolves.toMatchObject({ status: 'OPEN' });
|
||||
});
|
||||
|
||||
it('end, reactivate, end again the same day leaves one OBSOLETE and one OPEN CLEANUP suggestion', async () => {
|
||||
const ended = await endColony('reend');
|
||||
|
||||
await api()
|
||||
.post(`/api/v1/colonies/${ended.colonyId}/reactivate`)
|
||||
.set(auth(tokens.full))
|
||||
.expect(201);
|
||||
await api()
|
||||
.patch(`/api/v1/colonies/${ended.colonyId}`)
|
||||
.set(auth(tokens.full))
|
||||
.send({ status: 'DECEASED' })
|
||||
.expect(200);
|
||||
await drainDomainEvents(app, [ended.colonyId]);
|
||||
|
||||
const cleanups = await prisma.suggestion.findMany({
|
||||
where: { colonyId: ended.colonyId, intent: 'CLEANUP' },
|
||||
});
|
||||
expect(cleanups.map((row) => row.status).sort()).toEqual([
|
||||
'OBSOLETE',
|
||||
'OPEN',
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user