feat(backend): reactivate endpoint for dissolved colonies and inactive apiaries
Da fachliche Objekte nie hart geloescht werden (nur status + deactivatedAt), fehlte bisher ein Weg, ein versehentlich aufgeloestes Volk oder deaktivierten Bienenstand wieder zu aktivieren. Neue POST :id/reactivate-Endpunkte setzen status zurueck auf ACTIVE und loeschen deactivatedAt; ConflictException wenn bereits aktiv.
This commit is contained in:
1 parent
4a9fdd5c71
commit
6346bfab1d
8 files changed
+144
-2
No files matched your search
@@ -59,4 +59,10 @@ export class ApiariesController {
|
|||||||
deactivate(@Param('id') id: string, @CurrentUser() actor: AuthenticatedUser) {
|
deactivate(@Param('id') id: string, @CurrentUser() actor: AuthenticatedUser) {
|
||||||
return this.apiariesService.deactivate(id, actor.id);
|
return this.apiariesService.deactivate(id, actor.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Roles(Role.ADMIN, Role.BEEKEEPER)
|
||||||
|
@Post(':id/reactivate')
|
||||||
|
reactivate(@Param('id') id: string, @CurrentUser() actor: AuthenticatedUser) {
|
||||||
|
return this.apiariesService.reactivate(id, actor.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { NotFoundException } from '@nestjs/common';
|
import { ConflictException, NotFoundException } from '@nestjs/common';
|
||||||
import { ApiariesService } from './apiaries.service';
|
import { ApiariesService } from './apiaries.service';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { AuditLogService } from '../audit/audit-log.service';
|
import { AuditLogService } from '../audit/audit-log.service';
|
||||||
@@ -75,4 +75,35 @@ describe('ApiariesService', () => {
|
|||||||
expect.objectContaining({ entityType: 'Apiary', action: 'DEACTIVATE' }),
|
expect.objectContaining({ entityType: 'Apiary', action: 'DEACTIVATE' }),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('reactivate throws ConflictException when already active', async () => {
|
||||||
|
prisma.apiary.findUnique.mockResolvedValue(existingApiary);
|
||||||
|
await expect(
|
||||||
|
service.reactivate('apiary-1', 'user-1'),
|
||||||
|
).rejects.toBeInstanceOf(ConflictException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reactivate sets status to ACTIVE and clears deactivatedAt', async () => {
|
||||||
|
prisma.apiary.findUnique.mockResolvedValue({
|
||||||
|
...existingApiary,
|
||||||
|
status: 'INACTIVE',
|
||||||
|
deactivatedAt: new Date(),
|
||||||
|
});
|
||||||
|
prisma.apiary.update.mockResolvedValue({
|
||||||
|
...existingApiary,
|
||||||
|
status: 'ACTIVE',
|
||||||
|
deactivatedAt: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await service.reactivate('apiary-1', 'user-1');
|
||||||
|
|
||||||
|
expect(result.status).toBe('ACTIVE');
|
||||||
|
expect(prisma.apiary.update).toHaveBeenCalledWith({
|
||||||
|
where: { id: 'apiary-1' },
|
||||||
|
data: { status: 'ACTIVE', deactivatedAt: null },
|
||||||
|
});
|
||||||
|
expect(auditLog.record).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ entityType: 'Apiary', action: 'UPDATE' }),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
import {
|
||||||
|
ConflictException,
|
||||||
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { AuditLogService } from '../audit/audit-log.service';
|
import { AuditLogService } from '../audit/audit-log.service';
|
||||||
import { Apiary, ApiaryStatus } from '../../generated/prisma';
|
import { Apiary, ApiaryStatus } from '../../generated/prisma';
|
||||||
@@ -86,4 +90,24 @@ export class ApiariesService {
|
|||||||
});
|
});
|
||||||
return apiary;
|
return apiary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async reactivate(id: string, actingUserId: string): Promise<Apiary> {
|
||||||
|
const before = await this.findById(id);
|
||||||
|
if (before.status === 'ACTIVE') {
|
||||||
|
throw new ConflictException('Apiary is already active');
|
||||||
|
}
|
||||||
|
const apiary = await this.prisma.apiary.update({
|
||||||
|
where: { id },
|
||||||
|
data: { status: 'ACTIVE', deactivatedAt: null },
|
||||||
|
});
|
||||||
|
await this.auditLog.record({
|
||||||
|
entityType: 'Apiary',
|
||||||
|
entityId: id,
|
||||||
|
action: 'UPDATE',
|
||||||
|
userId: actingUserId,
|
||||||
|
beforeData: before,
|
||||||
|
afterData: apiary,
|
||||||
|
});
|
||||||
|
return apiary;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -97,4 +97,10 @@ export class ColoniesController {
|
|||||||
dissolve(@Param('id') id: string, @CurrentUser() actor: AuthenticatedUser) {
|
dissolve(@Param('id') id: string, @CurrentUser() actor: AuthenticatedUser) {
|
||||||
return this.coloniesService.dissolve(id, actor.id);
|
return this.coloniesService.dissolve(id, actor.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Roles(Role.ADMIN, Role.BEEKEEPER)
|
||||||
|
@Post(':id/reactivate')
|
||||||
|
reactivate(@Param('id') id: string, @CurrentUser() actor: AuthenticatedUser) {
|
||||||
|
return this.coloniesService.reactivate(id, actor.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -170,4 +170,35 @@ describe('ColoniesService', () => {
|
|||||||
expect.objectContaining({ action: 'DEACTIVATE' }),
|
expect.objectContaining({ action: 'DEACTIVATE' }),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('reactivate throws ConflictException when already active', async () => {
|
||||||
|
prisma.colony.findUnique.mockResolvedValue(parentColony);
|
||||||
|
await expect(
|
||||||
|
service.reactivate('colony-1', 'user-1'),
|
||||||
|
).rejects.toBeInstanceOf(ConflictException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reactivate sets status ACTIVE and clears deactivatedAt', async () => {
|
||||||
|
prisma.colony.findUnique.mockResolvedValue({
|
||||||
|
...parentColony,
|
||||||
|
status: 'DISSOLVED',
|
||||||
|
deactivatedAt: new Date(),
|
||||||
|
});
|
||||||
|
prisma.colony.update.mockResolvedValue({
|
||||||
|
...parentColony,
|
||||||
|
status: 'ACTIVE',
|
||||||
|
deactivatedAt: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await service.reactivate('colony-1', 'user-1');
|
||||||
|
|
||||||
|
expect(result.status).toBe('ACTIVE');
|
||||||
|
expect(prisma.colony.update).toHaveBeenCalledWith({
|
||||||
|
where: { id: 'colony-1' },
|
||||||
|
data: { status: 'ACTIVE', deactivatedAt: null },
|
||||||
|
});
|
||||||
|
expect(auditLog.record).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ action: 'UPDATE' }),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -212,6 +212,26 @@ export class ColoniesService {
|
|||||||
return colony;
|
return colony;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async reactivate(id: string, actingUserId: string): Promise<Colony> {
|
||||||
|
const before = await this.findById(id);
|
||||||
|
if (before.status === 'ACTIVE') {
|
||||||
|
throw new ConflictException('Colony is already active');
|
||||||
|
}
|
||||||
|
const colony = await this.prisma.colony.update({
|
||||||
|
where: { id },
|
||||||
|
data: { status: 'ACTIVE', deactivatedAt: null },
|
||||||
|
});
|
||||||
|
await this.auditLog.record({
|
||||||
|
entityType: 'Colony',
|
||||||
|
entityId: id,
|
||||||
|
action: 'UPDATE',
|
||||||
|
userId: actingUserId,
|
||||||
|
beforeData: before,
|
||||||
|
afterData: colony,
|
||||||
|
});
|
||||||
|
return colony;
|
||||||
|
}
|
||||||
|
|
||||||
async getHistory(id: string) {
|
async getHistory(id: string) {
|
||||||
await this.findById(id);
|
await this.findById(id);
|
||||||
const [apiaryHistory, queenHistory, lineageAsSource, lineageAsResult] =
|
const [apiaryHistory, queenHistory, lineageAsSource, lineageAsResult] =
|
||||||
|
|||||||
@@ -115,5 +115,16 @@ describe('Apiaries (e2e)', () => {
|
|||||||
.set('Authorization', `Bearer ${beekeeperToken}`)
|
.set('Authorization', `Bearer ${beekeeperToken}`)
|
||||||
.expect(201);
|
.expect(201);
|
||||||
expect((deactivated.body as ApiaryBody).status).toBe('INACTIVE');
|
expect((deactivated.body as ApiaryBody).status).toBe('INACTIVE');
|
||||||
|
|
||||||
|
const reactivated = await request(app.getHttpServer())
|
||||||
|
.post(`/api/v1/apiaries/${apiaryId}/reactivate`)
|
||||||
|
.set('Authorization', `Bearer ${beekeeperToken}`)
|
||||||
|
.expect(201);
|
||||||
|
expect((reactivated.body as ApiaryBody).status).toBe('ACTIVE');
|
||||||
|
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.post(`/api/v1/apiaries/${apiaryId}/reactivate`)
|
||||||
|
.set('Authorization', `Bearer ${beekeeperToken}`)
|
||||||
|
.expect(409);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -195,4 +195,17 @@ describe('Colonies (e2e)', () => {
|
|||||||
.send({ apiaryId: apiaryAId })
|
.send({ apiaryId: apiaryAId })
|
||||||
.expect(409);
|
.expect(409);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('reactivates a dissolved colony, and rejects reactivating an already-active one', async () => {
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post(`/api/v1/colonies/${colonyId}/reactivate`)
|
||||||
|
.set('Authorization', `Bearer ${beekeeperToken}`)
|
||||||
|
.expect(201);
|
||||||
|
expect((response.body as ColonyBody).status).toBe('ACTIVE');
|
||||||
|
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.post(`/api/v1/colonies/${colonyId}/reactivate`)
|
||||||
|
.set('Authorization', `Bearer ${beekeeperToken}`)
|
||||||
|
.expect(409);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user