diff --git a/ApiCore-Backend/src/app.module.ts b/ApiCore-Backend/src/app.module.ts index 97a09d5..0c8a36f 100644 --- a/ApiCore-Backend/src/app.module.ts +++ b/ApiCore-Backend/src/app.module.ts @@ -14,6 +14,8 @@ import { TasksModule } from './tasks/tasks.module'; import { VarroaModule } from './varroa/varroa.module'; import { TreatmentsModule } from './treatments/treatments.module'; import { FeedingsModule } from './feedings/feedings.module'; +import { HarvestsModule } from './harvests/harvests.module'; +import { ExtractionsModule } from './extractions/extractions.module'; import { JwtAuthGuard } from './auth/guards/jwt-auth.guard'; import { RolesGuard } from './auth/guards/roles.guard'; @@ -33,6 +35,8 @@ import { RolesGuard } from './auth/guards/roles.guard'; VarroaModule, TreatmentsModule, FeedingsModule, + HarvestsModule, + ExtractionsModule, ], controllers: [], providers: [ diff --git a/ApiCore-Backend/src/extractions/dto/create-extraction.dto.ts b/ApiCore-Backend/src/extractions/dto/create-extraction.dto.ts new file mode 100644 index 0000000..ada16de --- /dev/null +++ b/ApiCore-Backend/src/extractions/dto/create-extraction.dto.ts @@ -0,0 +1,46 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { + ArrayMinSize, + IsArray, + IsDateString, + IsNumber, + IsOptional, + IsString, +} from 'class-validator'; + +export class CreateExtractionDto { + @ApiProperty() + @IsDateString() + date!: string; + + @ApiProperty({ type: [String] }) + @IsArray() + @ArrayMinSize(1) + @IsString({ each: true }) + harvestIds!: string[]; + + @ApiPropertyOptional() + @IsOptional() + @IsNumber() + amount?: number; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + room?: string; + + @ApiPropertyOptional() + @IsOptional() + @IsNumber() + waterContent?: number; + + @ApiPropertyOptional() + @IsOptional() + @IsNumber() + temperature?: number; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + notes?: string; +} diff --git a/ApiCore-Backend/src/extractions/extractions.controller.ts b/ApiCore-Backend/src/extractions/extractions.controller.ts new file mode 100644 index 0000000..c6bfc5e --- /dev/null +++ b/ApiCore-Backend/src/extractions/extractions.controller.ts @@ -0,0 +1,34 @@ +import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { Role } from '../../generated/prisma'; +import { Roles } from '../auth/decorators/roles.decorator'; +import { CurrentUser } from '../auth/decorators/current-user.decorator'; +import type { AuthenticatedUser } from '../auth/types/authenticated-user'; +import { CreateExtractionDto } from './dto/create-extraction.dto'; +import { ExtractionsService } from './extractions.service'; + +@ApiTags('extractions') +@ApiBearerAuth() +@Controller('extractions') +export class ExtractionsController { + constructor(private readonly extractionsService: ExtractionsService) {} + + @Get() + findAll() { + return this.extractionsService.findAll(); + } + + @Get(':id') + findOne(@Param('id') id: string) { + return this.extractionsService.findById(id); + } + + @Roles(Role.ADMIN, Role.BEEKEEPER) + @Post() + create( + @Body() dto: CreateExtractionDto, + @CurrentUser() actor: AuthenticatedUser, + ) { + return this.extractionsService.create(dto, actor.id); + } +} diff --git a/ApiCore-Backend/src/extractions/extractions.module.ts b/ApiCore-Backend/src/extractions/extractions.module.ts new file mode 100644 index 0000000..2605485 --- /dev/null +++ b/ApiCore-Backend/src/extractions/extractions.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ExtractionsController } from './extractions.controller'; +import { ExtractionsService } from './extractions.service'; + +@Module({ + controllers: [ExtractionsController], + providers: [ExtractionsService], + exports: [ExtractionsService], +}) +export class ExtractionsModule {} diff --git a/ApiCore-Backend/src/extractions/extractions.service.spec.ts b/ApiCore-Backend/src/extractions/extractions.service.spec.ts new file mode 100644 index 0000000..1f0ce9e --- /dev/null +++ b/ApiCore-Backend/src/extractions/extractions.service.spec.ts @@ -0,0 +1,61 @@ +import { NotFoundException } from '@nestjs/common'; +import { ExtractionsService } from './extractions.service'; +import { PrismaService } from '../prisma/prisma.service'; +import { AuditLogService } from '../audit/audit-log.service'; + +describe('ExtractionsService', () => { + let service: ExtractionsService; + let prisma: { + extraction: { findUnique: jest.Mock }; + $transaction: jest.Mock; + }; + let auditLog: { record: jest.Mock }; + let tx: { + extraction: { create: jest.Mock }; + extractionHarvest: { createMany: jest.Mock }; + }; + + const extraction = { id: 'extraction-1', date: new Date() }; + + beforeEach(() => { + tx = { + extraction: { create: jest.fn().mockResolvedValue(extraction) }, + extractionHarvest: { createMany: jest.fn() }, + }; + prisma = { + extraction: { findUnique: jest.fn() }, + $transaction: jest.fn((callback: (txArg: typeof tx) => unknown) => + callback(tx), + ), + }; + auditLog = { record: jest.fn() }; + service = new ExtractionsService( + prisma as unknown as PrismaService, + auditLog as unknown as AuditLogService, + ); + }); + + it('findById throws NotFoundException when missing', async () => { + prisma.extraction.findUnique.mockResolvedValue(null); + await expect(service.findById('missing')).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + + it('create links every harvest to the extraction inside a transaction', async () => { + prisma.extraction.findUnique.mockResolvedValue(extraction); + + await service.create( + { date: '2026-07-05', harvestIds: ['harvest-1', 'harvest-2'] }, + 'user-1', + ); + + const [[createArg]] = tx.extractionHarvest.createMany.mock.calls as [ + [{ data: { extractionId: string; harvestId: string }[] }], + ]; + expect(createArg.data).toEqual([ + { extractionId: extraction.id, harvestId: 'harvest-1' }, + { extractionId: extraction.id, harvestId: 'harvest-2' }, + ]); + }); +}); diff --git a/ApiCore-Backend/src/extractions/extractions.service.ts b/ApiCore-Backend/src/extractions/extractions.service.ts new file mode 100644 index 0000000..3f94ec5 --- /dev/null +++ b/ApiCore-Backend/src/extractions/extractions.service.ts @@ -0,0 +1,69 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { AuditLogService } from '../audit/audit-log.service'; +import { CreateExtractionDto } from './dto/create-extraction.dto'; + +const extractionInclude = { + harvests: { + include: { + harvest: { include: { colonies: { include: { colony: true } } } }, + }, + }, +} as const; + +@Injectable() +export class ExtractionsService { + constructor( + private readonly prisma: PrismaService, + private readonly auditLog: AuditLogService, + ) {} + + async findAll() { + return this.prisma.extraction.findMany({ + include: extractionInclude, + orderBy: { date: 'desc' }, + }); + } + + async findById(id: string) { + const extraction = await this.prisma.extraction.findUnique({ + where: { id }, + include: extractionInclude, + }); + if (!extraction) { + throw new NotFoundException('Extraction not found'); + } + return extraction; + } + + async create(dto: CreateExtractionDto, actingUserId: string) { + const extraction = await this.prisma.$transaction(async (tx) => { + const created = await tx.extraction.create({ + data: { + date: new Date(dto.date), + amount: dto.amount, + room: dto.room, + waterContent: dto.waterContent, + temperature: dto.temperature, + notes: dto.notes, + userId: actingUserId, + }, + }); + await tx.extractionHarvest.createMany({ + data: dto.harvestIds.map((harvestId) => ({ + extractionId: created.id, + harvestId, + })), + }); + return created; + }); + await this.auditLog.record({ + entityType: 'Extraction', + entityId: extraction.id, + action: 'CREATE', + userId: actingUserId, + afterData: extraction, + }); + return this.findById(extraction.id); + } +} diff --git a/ApiCore-Backend/src/harvests/dto/create-harvest.dto.ts b/ApiCore-Backend/src/harvests/dto/create-harvest.dto.ts new file mode 100644 index 0000000..c1f606c --- /dev/null +++ b/ApiCore-Backend/src/harvests/dto/create-harvest.dto.ts @@ -0,0 +1,51 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { + ArrayMinSize, + IsArray, + IsDateString, + IsInt, + IsNumber, + IsOptional, + IsString, +} from 'class-validator'; + +export class CreateHarvestDto { + @ApiProperty() + @IsString() + apiaryId!: string; + + @ApiProperty() + @IsDateString() + date!: string; + + @ApiProperty({ type: [String] }) + @IsArray() + @ArrayMinSize(1) + @IsString({ each: true }) + colonyIds!: string[]; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + honeyType?: string; + + @ApiPropertyOptional() + @IsOptional() + @IsInt() + honeySuperCount?: number; + + @ApiPropertyOptional() + @IsOptional() + @IsNumber() + grossWeight?: number; + + @ApiPropertyOptional() + @IsOptional() + @IsNumber() + netWeight?: number; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + notes?: string; +} diff --git a/ApiCore-Backend/src/harvests/harvests.controller.ts b/ApiCore-Backend/src/harvests/harvests.controller.ts new file mode 100644 index 0000000..947a241 --- /dev/null +++ b/ApiCore-Backend/src/harvests/harvests.controller.ts @@ -0,0 +1,34 @@ +import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { Role } from '../../generated/prisma'; +import { Roles } from '../auth/decorators/roles.decorator'; +import { CurrentUser } from '../auth/decorators/current-user.decorator'; +import type { AuthenticatedUser } from '../auth/types/authenticated-user'; +import { CreateHarvestDto } from './dto/create-harvest.dto'; +import { HarvestsService } from './harvests.service'; + +@ApiTags('harvests') +@ApiBearerAuth() +@Controller('harvests') +export class HarvestsController { + constructor(private readonly harvestsService: HarvestsService) {} + + @Get() + findAll() { + return this.harvestsService.findAll(); + } + + @Get(':id') + findOne(@Param('id') id: string) { + return this.harvestsService.findById(id); + } + + @Roles(Role.ADMIN, Role.BEEKEEPER, Role.STAFF) + @Post() + create( + @Body() dto: CreateHarvestDto, + @CurrentUser() actor: AuthenticatedUser, + ) { + return this.harvestsService.create(dto, actor.id); + } +} diff --git a/ApiCore-Backend/src/harvests/harvests.module.ts b/ApiCore-Backend/src/harvests/harvests.module.ts new file mode 100644 index 0000000..f4e9e5d --- /dev/null +++ b/ApiCore-Backend/src/harvests/harvests.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { HarvestsController } from './harvests.controller'; +import { HarvestsService } from './harvests.service'; + +@Module({ + controllers: [HarvestsController], + providers: [HarvestsService], + exports: [HarvestsService], +}) +export class HarvestsModule {} diff --git a/ApiCore-Backend/src/harvests/harvests.service.spec.ts b/ApiCore-Backend/src/harvests/harvests.service.spec.ts new file mode 100644 index 0000000..f197096 --- /dev/null +++ b/ApiCore-Backend/src/harvests/harvests.service.spec.ts @@ -0,0 +1,65 @@ +import { NotFoundException } from '@nestjs/common'; +import { HarvestsService } from './harvests.service'; +import { PrismaService } from '../prisma/prisma.service'; +import { AuditLogService } from '../audit/audit-log.service'; + +describe('HarvestsService', () => { + let service: HarvestsService; + let prisma: { + harvest: { findUnique: jest.Mock }; + $transaction: jest.Mock; + }; + let auditLog: { record: jest.Mock }; + let tx: { + harvest: { create: jest.Mock }; + harvestColony: { createMany: jest.Mock }; + }; + + const harvest = { id: 'harvest-1', apiaryId: 'apiary-1', date: new Date() }; + + beforeEach(() => { + tx = { + harvest: { create: jest.fn().mockResolvedValue(harvest) }, + harvestColony: { createMany: jest.fn() }, + }; + prisma = { + harvest: { findUnique: jest.fn() }, + $transaction: jest.fn((callback: (txArg: typeof tx) => unknown) => + callback(tx), + ), + }; + auditLog = { record: jest.fn() }; + service = new HarvestsService( + prisma as unknown as PrismaService, + auditLog as unknown as AuditLogService, + ); + }); + + it('findById throws NotFoundException when missing', async () => { + prisma.harvest.findUnique.mockResolvedValue(null); + await expect(service.findById('missing')).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + + it('create links every colony to the harvest inside a transaction', async () => { + prisma.harvest.findUnique.mockResolvedValue(harvest); + + await service.create( + { + apiaryId: 'apiary-1', + date: '2026-07-01', + colonyIds: ['colony-1', 'colony-2'], + }, + 'user-1', + ); + + const [[createArg]] = tx.harvestColony.createMany.mock.calls as [ + [{ data: { harvestId: string; colonyId: string }[] }], + ]; + expect(createArg.data).toEqual([ + { harvestId: harvest.id, colonyId: 'colony-1' }, + { harvestId: harvest.id, colonyId: 'colony-2' }, + ]); + }); +}); diff --git a/ApiCore-Backend/src/harvests/harvests.service.ts b/ApiCore-Backend/src/harvests/harvests.service.ts new file mode 100644 index 0000000..2056b90 --- /dev/null +++ b/ApiCore-Backend/src/harvests/harvests.service.ts @@ -0,0 +1,66 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { AuditLogService } from '../audit/audit-log.service'; +import { CreateHarvestDto } from './dto/create-harvest.dto'; + +const harvestInclude = { + colonies: { include: { colony: true } }, +} as const; + +@Injectable() +export class HarvestsService { + constructor( + private readonly prisma: PrismaService, + private readonly auditLog: AuditLogService, + ) {} + + async findAll() { + return this.prisma.harvest.findMany({ + include: harvestInclude, + orderBy: { date: 'desc' }, + }); + } + + async findById(id: string) { + const harvest = await this.prisma.harvest.findUnique({ + where: { id }, + include: harvestInclude, + }); + if (!harvest) { + throw new NotFoundException('Harvest not found'); + } + return harvest; + } + + async create(dto: CreateHarvestDto, actingUserId: string) { + const harvest = await this.prisma.$transaction(async (tx) => { + const created = await tx.harvest.create({ + data: { + apiaryId: dto.apiaryId, + date: new Date(dto.date), + honeyType: dto.honeyType, + honeySuperCount: dto.honeySuperCount, + grossWeight: dto.grossWeight, + netWeight: dto.netWeight, + notes: dto.notes, + userId: actingUserId, + }, + }); + await tx.harvestColony.createMany({ + data: dto.colonyIds.map((colonyId) => ({ + harvestId: created.id, + colonyId, + })), + }); + return created; + }); + await this.auditLog.record({ + entityType: 'Harvest', + entityId: harvest.id, + action: 'CREATE', + userId: actingUserId, + afterData: harvest, + }); + return this.findById(harvest.id); + } +} diff --git a/ApiCore-Backend/test/extractions.e2e-spec.ts b/ApiCore-Backend/test/extractions.e2e-spec.ts new file mode 100644 index 0000000..7f74314 --- /dev/null +++ b/ApiCore-Backend/test/extractions.e2e-spec.ts @@ -0,0 +1,121 @@ +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 { createTestApp } from './utils/create-test-app'; +import { login } from './utils/api'; + +interface ExtractionBody { + id: string; + harvests: { harvestId: string }[]; +} + +describe('Extractions (e2e)', () => { + let app: INestApplication; + let prisma: PrismaService; + const beekeeperEmail = `extractions-e2e-beekeeper-${Date.now()}@example.com`; + const staffEmail = `extractions-e2e-staff-${Date.now()}@example.com`; + const password = 'SuperSecret123!'; + let beekeeperId: string; + let staffId: string; + let beekeeperToken: string; + let staffToken: string; + let apiaryId: string; + let colonyId: string; + let harvestId: string; + let extractionId: string; + + beforeAll(async () => { + app = await createTestApp(); + prisma = app.get(PrismaService); + + const passwordHash = await bcrypt.hash(password, 12); + const beekeeper = await prisma.user.create({ + data: { + email: beekeeperEmail, + passwordHash, + name: 'Beekeeper', + role: 'BEEKEEPER', + }, + }); + beekeeperId = beekeeper.id; + const staff = await prisma.user.create({ + data: { email: staffEmail, passwordHash, name: 'Staff', role: 'STAFF' }, + }); + staffId = staff.id; + beekeeperToken = (await login(app, beekeeperEmail, password)).accessToken; + staffToken = (await login(app, staffEmail, password)).accessToken; + + const apiary = await prisma.apiary.create({ + data: { name: `Stand ${Date.now()}` }, + }); + apiaryId = apiary.id; + const colony = await prisma.colony.create({ + data: { + code: `V-${Date.now()}`, + apiaryId, + colonyType: 'PRODUCTION_COLONY', + }, + }); + colonyId = colony.id; + const harvest = await prisma.harvest.create({ + data: { apiaryId, date: new Date(), userId: beekeeperId }, + }); + harvestId = harvest.id; + await prisma.harvestColony.create({ data: { harvestId, colonyId } }); + }); + + afterAll(async () => { + if (extractionId) { + await prisma.extractionHarvest.deleteMany({ where: { extractionId } }); + await prisma.extraction.deleteMany({ where: { id: extractionId } }); + } + await prisma.harvestColony.deleteMany({ where: { harvestId } }); + await prisma.harvest.deleteMany({ where: { id: harvestId } }); + await prisma.colony.deleteMany({ where: { id: colonyId } }); + await prisma.apiary.deleteMany({ where: { id: apiaryId } }); + await prisma.refreshToken.deleteMany({ + where: { userId: { in: [beekeeperId, staffId] } }, + }); + await prisma.auditLog.deleteMany({ + where: { userId: { in: [beekeeperId, staffId] } }, + }); + await prisma.user.deleteMany({ + where: { id: { in: [beekeeperId, staffId] } }, + }); + await app.close(); + }); + + it('rejects create for a STAFF user', () => { + return request(app.getHttpServer()) + .post('/api/v1/extractions') + .set('Authorization', `Bearer ${staffToken}`) + .send({ date: new Date().toISOString(), harvestIds: [harvestId] }) + .expect(403); + }); + + it('lets a BEEKEEPER user record an extraction linked to a harvest', async () => { + const response = await request(app.getHttpServer()) + .post('/api/v1/extractions') + .set('Authorization', `Bearer ${beekeeperToken}`) + .send({ + date: new Date().toISOString(), + harvestIds: [harvestId], + amount: 15.2, + }) + .expect(201); + const extraction = response.body as ExtractionBody; + extractionId = extraction.id; + expect(extraction.harvests).toHaveLength(1); + }); + + it('returns the extraction with its source harvests on lookup', async () => { + const response = await request(app.getHttpServer()) + .get(`/api/v1/extractions/${extractionId}`) + .set('Authorization', `Bearer ${staffToken}`) + .expect(200); + const extraction = response.body as ExtractionBody; + expect(extraction.harvests[0]?.harvestId).toBe(harvestId); + }); +}); diff --git a/ApiCore-Backend/test/harvests.e2e-spec.ts b/ApiCore-Backend/test/harvests.e2e-spec.ts new file mode 100644 index 0000000..1712bc1 --- /dev/null +++ b/ApiCore-Backend/test/harvests.e2e-spec.ts @@ -0,0 +1,133 @@ +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 { createTestApp } from './utils/create-test-app'; +import { login } from './utils/api'; + +interface HarvestBody { + id: string; + apiaryId: string; + colonies: { colonyId: string }[]; +} + +describe('Harvests (e2e)', () => { + let app: INestApplication; + let prisma: PrismaService; + const staffEmail = `harvests-e2e-staff-${Date.now()}@example.com`; + const readonlyEmail = `harvests-e2e-readonly-${Date.now()}@example.com`; + const password = 'SuperSecret123!'; + let staffId: string; + let readonlyId: string; + let staffToken: string; + let readonlyToken: string; + let apiaryId: string; + let colonyOneId: string; + let colonyTwoId: string; + let harvestId: string; + + beforeAll(async () => { + app = await createTestApp(); + prisma = app.get(PrismaService); + + const passwordHash = await bcrypt.hash(password, 12); + const staff = await prisma.user.create({ + data: { email: staffEmail, passwordHash, name: 'Staff', role: 'STAFF' }, + }); + staffId = staff.id; + const readonly = await prisma.user.create({ + data: { + email: readonlyEmail, + passwordHash, + name: 'Readonly', + role: 'READONLY', + }, + }); + readonlyId = readonly.id; + staffToken = (await login(app, staffEmail, password)).accessToken; + readonlyToken = (await login(app, readonlyEmail, password)).accessToken; + + const apiary = await prisma.apiary.create({ + data: { name: `Stand ${Date.now()}` }, + }); + apiaryId = apiary.id; + const colonyOne = await prisma.colony.create({ + data: { + code: `V-${Date.now()}-A`, + apiaryId, + colonyType: 'PRODUCTION_COLONY', + }, + }); + colonyOneId = colonyOne.id; + const colonyTwo = await prisma.colony.create({ + data: { + code: `V-${Date.now()}-B`, + apiaryId, + colonyType: 'PRODUCTION_COLONY', + }, + }); + colonyTwoId = colonyTwo.id; + }); + + afterAll(async () => { + if (harvestId) { + await prisma.harvestColony.deleteMany({ where: { harvestId } }); + await prisma.harvest.deleteMany({ where: { id: harvestId } }); + } + await prisma.colony.deleteMany({ + where: { id: { in: [colonyOneId, colonyTwoId] } }, + }); + await prisma.apiary.deleteMany({ where: { id: apiaryId } }); + await prisma.refreshToken.deleteMany({ + where: { userId: { in: [staffId, readonlyId] } }, + }); + await prisma.auditLog.deleteMany({ + where: { userId: { in: [staffId, readonlyId] } }, + }); + await prisma.user.deleteMany({ + where: { id: { in: [staffId, readonlyId] } }, + }); + await app.close(); + }); + + it('rejects create for a READONLY user', () => { + return request(app.getHttpServer()) + .post('/api/v1/harvests') + .set('Authorization', `Bearer ${readonlyToken}`) + .send({ + apiaryId, + date: new Date().toISOString(), + colonyIds: [colonyOneId], + }) + .expect(403); + }); + + it('lets a STAFF user record a harvest across multiple colonies', async () => { + const response = await request(app.getHttpServer()) + .post('/api/v1/harvests') + .set('Authorization', `Bearer ${staffToken}`) + .send({ + apiaryId, + date: new Date().toISOString(), + colonyIds: [colonyOneId, colonyTwoId], + netWeight: 18.5, + }) + .expect(201); + const harvest = response.body as HarvestBody; + harvestId = harvest.id; + expect(harvest.colonies).toHaveLength(2); + }); + + it('returns the harvest with its colonies on lookup', async () => { + const response = await request(app.getHttpServer()) + .get(`/api/v1/harvests/${harvestId}`) + .set('Authorization', `Bearer ${readonlyToken}`) + .expect(200); + const harvest = response.body as HarvestBody; + const colonyIds = harvest.colonies.map((entry) => entry.colonyId); + expect(colonyIds).toEqual( + expect.arrayContaining([colonyOneId, colonyTwoId]), + ); + }); +}); diff --git a/TASKS.md b/TASKS.md index 9e2765d..d4b8e38 100644 --- a/TASKS.md +++ b/TASKS.md @@ -90,7 +90,7 @@ Abgeleitet aus [`docs/lastenheft.md`](docs/lastenheft.md) Abschnitt 26. Reihenfo - Backend: `HarvestsModule`, `ExtractionsModule` - Endpunkte: `GET/POST /api/v1/harvests`, `GET /api/v1/harvests/:id`, `GET/POST /api/v1/extractions`, `GET /api/v1/extractions/:id` - Frontend: `/harvests/new`, `/harvests/[id]`, `/extractions/new` -- [ ] Backend-Modul fertig, getestet, grün, committet +- [x] Backend-Modul fertig, getestet, grün, committet - [ ] Frontend-Views fertig, getestet, grün, committet ### 11. Honigchargen