feat: platform backbone and field capture complete (checkpoint)
Platform backbone (plan 2026-10-13-plattform-rueckgrat), all tasks reviewed: - webhooks with delivery table, retries and stable delivery id - calendar handlers; a finished task removes its Outlook event - tasks, automations, inspections, varroa, treatments, feedings, harvests, colonies, wintering, stock and bottling record domain events in the same transaction; legacy post-commit calls removed - row locks through one helper (lockRowForUpdate, lockRowsForShare, lockRowsForUpdate) - job framework with JobRun, cli exit codes, cleanup, task templates, daily evaluation, hourly app-backup and the rekey job - system API and page "Verwaltung > System" with retry - @nestjs/schedule removed, guard tests against in-process schedulers Field capture T3 (plan 2026-10-13-t3-erfassung-am-stand), all tasks reviewed: - inspection photo offline without metadata - bulk capture for treatments, feedings and varroa (backend + frontend) - offline stand mode with service worker, offline pages and packs - offline session, cold start and logout on shared devices Docs: ARCHITECTURE.md assumptions and sections updated, TASKS.md. Gates: backend lint, typecheck, 1770 unit, 420 e2e, build, no schema drift; frontend lint, typecheck, 913 tests, build. Not rolled out. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
c01a38835a
commit
cdd30cf3ee
288 files changed
+39165
-2853
No files matched your search
@@ -1,11 +1,15 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AutomationModule } from '../automation/automation.module';
|
||||
import { TasksModule } from '../tasks/tasks.module';
|
||||
import { InspectionsController } from './inspections.controller';
|
||||
import { ColonyInspectionsController } from './colony-inspections.controller';
|
||||
import { InspectionsService } from './inspections.service';
|
||||
|
||||
/**
|
||||
* Rules, calendar and webhooks follow from the inspection's domain events
|
||||
* (DomainEventsModule is global), so no AutomationModule here.
|
||||
*/
|
||||
@Module({
|
||||
imports: [AutomationModule],
|
||||
imports: [TasksModule],
|
||||
controllers: [InspectionsController, ColonyInspectionsController],
|
||||
providers: [InspectionsService],
|
||||
exports: [InspectionsService],
|
||||
|
||||
@@ -1,24 +1,30 @@
|
||||
import { NotFoundException } from '@nestjs/common';
|
||||
import { InspectionsService } from './inspections.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { WebhooksService } from '../webhooks/webhooks.service';
|
||||
import { AutomationService } from '../automation/automation.service';
|
||||
import { AuditLogService } from '../audit/audit-log.service';
|
||||
import { CalendarSyncService } from '../calendar-sync/calendar-sync.service';
|
||||
import { DomainEventsService } from '../domain-events/domain-events.service';
|
||||
import { TasksService } from '../tasks/tasks.service';
|
||||
import { Prisma } from '../../generated/prisma';
|
||||
|
||||
describe('InspectionsService', () => {
|
||||
let service: InspectionsService;
|
||||
let tx: {
|
||||
$queryRaw: jest.Mock;
|
||||
inspection: { findUnique: jest.Mock; create: jest.Mock; update: jest.Mock };
|
||||
};
|
||||
let prisma: {
|
||||
$connect: jest.Mock;
|
||||
$transaction: jest.Mock;
|
||||
inspection: {
|
||||
findMany: jest.Mock;
|
||||
findUnique: jest.Mock;
|
||||
create: jest.Mock;
|
||||
update: jest.Mock;
|
||||
};
|
||||
task: { create: jest.Mock };
|
||||
};
|
||||
let auditLog: { record: jest.Mock };
|
||||
let calendarSync: { enqueue: jest.Mock; cancel: jest.Mock };
|
||||
let events: { record: jest.Mock };
|
||||
let tasks: { createInTx: jest.Mock };
|
||||
|
||||
const inspection = {
|
||||
id: 'inspection-1',
|
||||
@@ -27,30 +33,48 @@ describe('InspectionsService', () => {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
tx = {
|
||||
$queryRaw: jest.fn().mockResolvedValue([]),
|
||||
inspection: {
|
||||
findUnique: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
},
|
||||
};
|
||||
prisma = {
|
||||
$connect: jest.fn(),
|
||||
$transaction: jest.fn((work: (client: typeof tx) => Promise<unknown>) =>
|
||||
work(tx),
|
||||
),
|
||||
inspection: {
|
||||
findMany: jest.fn(),
|
||||
findUnique: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
},
|
||||
task: { create: jest.fn() },
|
||||
};
|
||||
const webhooks = { dispatch: jest.fn().mockResolvedValue(undefined) };
|
||||
const automation = {
|
||||
evaluateInspection: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
auditLog = { record: jest.fn().mockResolvedValue(undefined) };
|
||||
calendarSync = { enqueue: jest.fn(), cancel: jest.fn() };
|
||||
events = { record: jest.fn().mockResolvedValue(undefined) };
|
||||
tasks = { createInTx: jest.fn().mockResolvedValue({ id: 'task-1' }) };
|
||||
service = new InspectionsService(
|
||||
prisma as unknown as PrismaService,
|
||||
webhooks as unknown as WebhooksService,
|
||||
automation as unknown as AutomationService,
|
||||
auditLog as unknown as AuditLogService,
|
||||
calendarSync as unknown as CalendarSyncService,
|
||||
events as unknown as DomainEventsService,
|
||||
tasks as unknown as TasksService,
|
||||
);
|
||||
});
|
||||
|
||||
it('has no side-effect dependency: rules, calendar and webhooks follow from its events', () => {
|
||||
expect(
|
||||
Reflect.getMetadata('design:paramtypes', InspectionsService),
|
||||
).toEqual([
|
||||
PrismaService,
|
||||
AuditLogService,
|
||||
DomainEventsService,
|
||||
TasksService,
|
||||
]);
|
||||
});
|
||||
|
||||
it('findById throws NotFoundException when missing', async () => {
|
||||
prisma.inspection.findUnique.mockResolvedValue(null);
|
||||
await expect(service.findById('missing')).rejects.toBeInstanceOf(
|
||||
@@ -58,96 +82,180 @@ describe('InspectionsService', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('create stores the acting user as the inspector', async () => {
|
||||
prisma.inspection.create.mockResolvedValue(inspection);
|
||||
it('create writes inspection, audit entry and InspectionRecorded in one transaction and calls no side effect directly', async () => {
|
||||
tx.inspection.create.mockResolvedValue(inspection);
|
||||
|
||||
await service.create(
|
||||
const result = await service.create(
|
||||
{ colonyId: 'colony-1', date: '2026-01-01' },
|
||||
'user-1',
|
||||
);
|
||||
|
||||
const [[createArg]] = prisma.inspection.create.mock.calls as [
|
||||
[{ data: { colonyId: string; userId: string } }],
|
||||
];
|
||||
expect(createArg.data.colonyId).toBe('colony-1');
|
||||
expect(createArg.data.userId).toBe('user-1');
|
||||
});
|
||||
|
||||
it('create writes an audit log entry with the created inspection', async () => {
|
||||
prisma.inspection.create.mockResolvedValue(inspection);
|
||||
|
||||
await service.create(
|
||||
{ colonyId: 'colony-1', date: '2026-01-01' },
|
||||
'user-1',
|
||||
);
|
||||
|
||||
expect(auditLog.record).toHaveBeenCalledWith({
|
||||
entityType: 'Inspection',
|
||||
entityId: 'inspection-1',
|
||||
action: 'CREATE',
|
||||
userId: 'user-1',
|
||||
afterData: inspection,
|
||||
expect(result).toBe(inspection);
|
||||
expect(prisma.$transaction).toHaveBeenCalledTimes(1);
|
||||
expect(tx.inspection.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
colonyId: 'colony-1',
|
||||
date: new Date('2026-01-01'),
|
||||
userId: 'user-1',
|
||||
},
|
||||
});
|
||||
expect(auditLog.record).toHaveBeenCalledWith(
|
||||
{
|
||||
entityType: 'Inspection',
|
||||
entityId: 'inspection-1',
|
||||
action: 'CREATE',
|
||||
userId: 'user-1',
|
||||
afterData: inspection,
|
||||
},
|
||||
tx,
|
||||
);
|
||||
expect(events.record).toHaveBeenCalledWith(tx, {
|
||||
type: 'InspectionRecorded',
|
||||
aggregateType: 'Inspection',
|
||||
aggregateId: 'inspection-1',
|
||||
payload: { colonyId: 'colony-1' },
|
||||
actorUserId: 'user-1',
|
||||
});
|
||||
// Nothing is written outside the transaction - no rule evaluation,
|
||||
// calendar entry or webhook runs in the request.
|
||||
expect(prisma.inspection.create).not.toHaveBeenCalled();
|
||||
expect(tasks.createInTx).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('update writes an audit log entry with before and after data', async () => {
|
||||
prisma.inspection.findUnique.mockResolvedValue(inspection);
|
||||
const updated = { ...inspection, notes: 'geändert' };
|
||||
prisma.inspection.update.mockResolvedValue(updated);
|
||||
it('create fails as a whole when its event cannot be written', async () => {
|
||||
tx.inspection.create.mockResolvedValue(inspection);
|
||||
events.record.mockRejectedValue(new Error('outbox down'));
|
||||
|
||||
await expect(
|
||||
service.create({ colonyId: 'colony-1', date: '2026-01-01' }, 'user-1'),
|
||||
).rejects.toThrow('outbox down');
|
||||
});
|
||||
|
||||
it('update writes the correction, audit entry and InspectionCorrected in one transaction', async () => {
|
||||
tx.inspection.findUnique.mockResolvedValue(inspection);
|
||||
// Re-filed against another colony: the event names the colony it is
|
||||
// filed against now.
|
||||
const updated = { ...inspection, colonyId: 'colony-2', notes: 'geändert' };
|
||||
tx.inspection.update.mockResolvedValue(updated);
|
||||
|
||||
const result = await service.update(
|
||||
'inspection-1',
|
||||
{ notes: 'geändert' },
|
||||
{ colonyId: 'colony-2', notes: 'geändert', date: '2026-05-02' },
|
||||
'user-1',
|
||||
);
|
||||
|
||||
expect(result).toBe(updated);
|
||||
expect(prisma.$transaction).toHaveBeenCalledTimes(1);
|
||||
expect(tx.inspection.findUnique).toHaveBeenCalledWith({
|
||||
where: { id: 'inspection-1' },
|
||||
});
|
||||
expect(tx.inspection.update).toHaveBeenCalledWith({
|
||||
where: { id: 'inspection-1' },
|
||||
data: {
|
||||
colonyId: 'colony-2',
|
||||
notes: 'geändert',
|
||||
date: new Date('2026-05-02'),
|
||||
},
|
||||
});
|
||||
expect(auditLog.record).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
{
|
||||
entityType: 'Inspection',
|
||||
entityId: 'inspection-1',
|
||||
action: 'UPDATE',
|
||||
userId: 'user-1',
|
||||
beforeData: inspection,
|
||||
afterData: updated,
|
||||
}),
|
||||
},
|
||||
tx,
|
||||
);
|
||||
// The calendar entry follows from the event (calendar-sync.inspection);
|
||||
// a correction evaluates no automation rule.
|
||||
expect(events.record).toHaveBeenCalledWith(tx, {
|
||||
type: 'InspectionCorrected',
|
||||
aggregateType: 'Inspection',
|
||||
aggregateId: 'inspection-1',
|
||||
payload: { colonyId: 'colony-2' },
|
||||
actorUserId: 'user-1',
|
||||
});
|
||||
expect(prisma.inspection.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('update locks the inspection row before re-reading it as "before"', async () => {
|
||||
tx.inspection.findUnique.mockResolvedValue(inspection);
|
||||
tx.inspection.update.mockResolvedValue(inspection);
|
||||
|
||||
await service.update('inspection-1', { notes: 'x' }, 'user-1');
|
||||
|
||||
const [[lock]] = tx.$queryRaw.mock.calls as [[Prisma.Sql]];
|
||||
expect(lock.sql).toBe(
|
||||
'SELECT id FROM "Inspection" WHERE id = ? FOR UPDATE',
|
||||
);
|
||||
expect(lock.values).toEqual(['inspection-1']);
|
||||
const [lockedAt] = tx.$queryRaw.mock.invocationCallOrder;
|
||||
const [readAt] = tx.inspection.findUnique.mock.invocationCallOrder;
|
||||
expect(lockedAt).toBeLessThan(readAt);
|
||||
});
|
||||
|
||||
it('update uses the locked row, not a read outside the transaction, as "before"', async () => {
|
||||
// Another request re-filed the inspection while this one waited.
|
||||
prisma.inspection.findUnique.mockResolvedValue(inspection);
|
||||
const locked = { ...inspection, colonyId: 'colony-3', notes: 'parallel' };
|
||||
tx.inspection.findUnique.mockResolvedValue(locked);
|
||||
const updated = { ...locked, notes: 'geändert' };
|
||||
tx.inspection.update.mockResolvedValue(updated);
|
||||
|
||||
await service.update('inspection-1', { notes: 'geändert' }, 'user-1');
|
||||
|
||||
expect(prisma.inspection.findUnique).not.toHaveBeenCalled();
|
||||
expect(auditLog.record).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ beforeData: locked, afterData: updated }),
|
||||
tx,
|
||||
);
|
||||
});
|
||||
|
||||
it('update re-enqueues the calendar sync so a changed date moves the event', async () => {
|
||||
prisma.inspection.findUnique.mockResolvedValue(inspection);
|
||||
prisma.inspection.update.mockResolvedValue(inspection);
|
||||
it('update throws NotFoundException for a missing inspection and writes nothing', async () => {
|
||||
tx.inspection.findUnique.mockResolvedValue(null);
|
||||
|
||||
await service.update('inspection-1', { date: '2026-05-02' }, 'user-1');
|
||||
|
||||
expect(calendarSync.enqueue).toHaveBeenCalledWith(
|
||||
'INSPECTION',
|
||||
'inspection-1',
|
||||
);
|
||||
await expect(
|
||||
service.update('missing', { notes: 'x' }, 'user-1'),
|
||||
).rejects.toBeInstanceOf(NotFoundException);
|
||||
expect(tx.inspection.update).not.toHaveBeenCalled();
|
||||
expect(auditLog.record).not.toHaveBeenCalled();
|
||||
expect(events.record).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('createTaskFromInspection links the task to the inspection and its colony', async () => {
|
||||
it('createTaskFromInspection creates the task through TasksService.createInTx in one transaction', async () => {
|
||||
prisma.inspection.findUnique.mockResolvedValue(inspection);
|
||||
prisma.task.create.mockResolvedValue({ id: 'task-1' });
|
||||
|
||||
await service.createTaskFromInspection(
|
||||
const result = await service.createTaskFromInspection(
|
||||
'inspection-1',
|
||||
{ title: 'Nachschauen' },
|
||||
{
|
||||
title: 'Nachschauen',
|
||||
priority: 'HIGH',
|
||||
dueDate: '2026-06-01T00:00:00.000Z',
|
||||
},
|
||||
'user-1',
|
||||
);
|
||||
|
||||
const [[taskArg]] = prisma.task.create.mock.calls as [
|
||||
[
|
||||
{
|
||||
data: {
|
||||
sourceInspectionId: string;
|
||||
relatedType: string;
|
||||
relatedId: string;
|
||||
};
|
||||
},
|
||||
],
|
||||
];
|
||||
expect(taskArg.data.sourceInspectionId).toBe('inspection-1');
|
||||
expect(taskArg.data.relatedType).toBe('COLONY');
|
||||
expect(taskArg.data.relatedId).toBe('colony-1');
|
||||
expect(result).toEqual({ id: 'task-1' });
|
||||
expect(prisma.$transaction).toHaveBeenCalledTimes(1);
|
||||
expect(tasks.createInTx).toHaveBeenCalledWith(
|
||||
tx,
|
||||
{
|
||||
title: 'Nachschauen',
|
||||
description: undefined,
|
||||
priority: 'HIGH',
|
||||
dueDate: new Date('2026-06-01T00:00:00.000Z'),
|
||||
assignedUserId: undefined,
|
||||
relatedType: 'COLONY',
|
||||
relatedId: 'colony-1',
|
||||
sourceInspectionId: 'inspection-1',
|
||||
},
|
||||
'user-1',
|
||||
);
|
||||
// Audit entry and TaskCreated come from createInTx - calendar entry and
|
||||
// webhook follow from that event, not from here.
|
||||
expect(auditLog.record).not.toHaveBeenCalled();
|
||||
expect(events.record).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,23 +1,31 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { WebhooksService } from '../webhooks/webhooks.service';
|
||||
import { AutomationService } from '../automation/automation.service';
|
||||
import { DbClient } from '../prisma/db-client';
|
||||
import { lockRowForUpdate } from '../prisma/row-lock';
|
||||
import { AuditLogService } from '../audit/audit-log.service';
|
||||
import { CalendarSyncService } from '../calendar-sync/calendar-sync.service';
|
||||
import { DomainEventsService } from '../domain-events/domain-events.service';
|
||||
import { TasksService } from '../tasks/tasks.service';
|
||||
import { Inspection, Task } from '../../generated/prisma';
|
||||
import { CreateInspectionDto } from './dto/create-inspection.dto';
|
||||
import { CreateTaskFromInspectionDto } from './dto/create-task-from-inspection.dto';
|
||||
import { UpdateInspectionDto } from './dto/update-inspection.dto';
|
||||
import { optionalDate } from '../common/patch-values';
|
||||
|
||||
/**
|
||||
* Inspections are written together with their audit entry and domain event
|
||||
* in one transaction: InspectionRecorded, InspectionCorrected. Automation
|
||||
* rules (automation.inspection-rules, on InspectionRecorded only), calendar
|
||||
* entry (calendar-sync.inspection) and the inspection.created webhook
|
||||
* (webhooks.forward) follow from the event, never from a direct call - so
|
||||
* none of them can fail the request or run twice on a retried one.
|
||||
*/
|
||||
@Injectable()
|
||||
export class InspectionsService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly webhooks: WebhooksService,
|
||||
private readonly automation: AutomationService,
|
||||
private readonly auditLog: AuditLogService,
|
||||
private readonly calendarSync: CalendarSyncService,
|
||||
private readonly events: DomainEventsService,
|
||||
private readonly tasks: TasksService,
|
||||
) {}
|
||||
|
||||
async findAll(): Promise<Inspection[]> {
|
||||
@@ -25,13 +33,7 @@ export class InspectionsService {
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Inspection> {
|
||||
const inspection = await this.prisma.inspection.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
if (!inspection) {
|
||||
throw new NotFoundException('Inspection not found');
|
||||
}
|
||||
return inspection;
|
||||
return this.findIn(this.prisma, id);
|
||||
}
|
||||
|
||||
async findByColony(colonyId: string): Promise<Inspection[]> {
|
||||
@@ -45,78 +47,104 @@ export class InspectionsService {
|
||||
dto: CreateInspectionDto,
|
||||
actingUserId: string,
|
||||
): Promise<Inspection> {
|
||||
const inspection = await this.prisma.inspection.create({
|
||||
data: { ...dto, date: new Date(dto.date), userId: actingUserId },
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
const inspection = await tx.inspection.create({
|
||||
data: { ...dto, date: new Date(dto.date), userId: actingUserId },
|
||||
});
|
||||
await this.auditLog.record(
|
||||
{
|
||||
entityType: 'Inspection',
|
||||
entityId: inspection.id,
|
||||
action: 'CREATE',
|
||||
userId: actingUserId,
|
||||
afterData: inspection,
|
||||
},
|
||||
tx,
|
||||
);
|
||||
await this.events.record(tx, {
|
||||
type: 'InspectionRecorded',
|
||||
aggregateType: 'Inspection',
|
||||
aggregateId: inspection.id,
|
||||
payload: { colonyId: inspection.colonyId },
|
||||
actorUserId: actingUserId,
|
||||
});
|
||||
return inspection;
|
||||
});
|
||||
await this.auditLog.record({
|
||||
entityType: 'Inspection',
|
||||
entityId: inspection.id,
|
||||
action: 'CREATE',
|
||||
userId: actingUserId,
|
||||
afterData: inspection,
|
||||
});
|
||||
await this.webhooks.dispatch('inspection.created', inspection);
|
||||
await this.automation.evaluateInspection(inspection);
|
||||
await this.calendarSync.enqueue('INSPECTION', inspection.id);
|
||||
return inspection;
|
||||
}
|
||||
|
||||
/**
|
||||
* A correction evaluates no automation rule, as before the event
|
||||
* backbone. InspectionCorrected moves the calendar entry when the date or
|
||||
* colony changed instead of leaving the old one behind.
|
||||
*/
|
||||
async update(
|
||||
id: string,
|
||||
dto: UpdateInspectionDto,
|
||||
actingUserId: string,
|
||||
): Promise<Inspection> {
|
||||
const before = await this.findById(id);
|
||||
const inspection = await this.prisma.inspection.update({
|
||||
where: { id },
|
||||
data: { ...dto, date: dto.date ? new Date(dto.date) : undefined },
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
// "before" is read under the row lock: a concurrent correction can
|
||||
// neither slip in between nor be mislabelled in the audit entry.
|
||||
await lockRowForUpdate(tx, 'Inspection', id);
|
||||
const before = await this.findIn(tx, id);
|
||||
const inspection = await tx.inspection.update({
|
||||
where: { id },
|
||||
data: { ...dto, date: dto.date ? new Date(dto.date) : undefined },
|
||||
});
|
||||
await this.auditLog.record(
|
||||
{
|
||||
entityType: 'Inspection',
|
||||
entityId: id,
|
||||
action: 'UPDATE',
|
||||
userId: actingUserId,
|
||||
beforeData: before,
|
||||
afterData: inspection,
|
||||
},
|
||||
tx,
|
||||
);
|
||||
await this.events.record(tx, {
|
||||
type: 'InspectionCorrected',
|
||||
aggregateType: 'Inspection',
|
||||
aggregateId: id,
|
||||
payload: { colonyId: inspection.colonyId },
|
||||
actorUserId: actingUserId,
|
||||
});
|
||||
return inspection;
|
||||
});
|
||||
await this.auditLog.record({
|
||||
entityType: 'Inspection',
|
||||
entityId: id,
|
||||
action: 'UPDATE',
|
||||
userId: actingUserId,
|
||||
beforeData: before,
|
||||
afterData: inspection,
|
||||
});
|
||||
// A changed date or colony must move the calendar event, not leave the
|
||||
// old one behind.
|
||||
await this.calendarSync.enqueue('INSPECTION', inspection.id);
|
||||
return inspection;
|
||||
}
|
||||
|
||||
// Through TasksService like a task from the task page: audit entry,
|
||||
// TaskCreated and with it calendar entry and webhook do not depend on
|
||||
// where the task was started.
|
||||
async createTaskFromInspection(
|
||||
id: string,
|
||||
dto: CreateTaskFromInspectionDto,
|
||||
actingUserId: string,
|
||||
): Promise<Task> {
|
||||
const inspection = await this.findById(id);
|
||||
const task = await this.prisma.task.create({
|
||||
data: {
|
||||
title: dto.title,
|
||||
description: dto.description,
|
||||
priority: dto.priority,
|
||||
dueDate: optionalDate(dto.dueDate),
|
||||
assignedUserId: dto.assignedUserId,
|
||||
createdByUserId: actingUserId,
|
||||
relatedType: 'COLONY',
|
||||
relatedId: inspection.colonyId,
|
||||
sourceInspectionId: inspection.id,
|
||||
},
|
||||
});
|
||||
// Same trail as a task created on the task page: audit entry, webhook
|
||||
// and calendar entry must not depend on where the task was started.
|
||||
await this.auditLog.record({
|
||||
entityType: 'Task',
|
||||
entityId: task.id,
|
||||
action: 'CREATE',
|
||||
userId: actingUserId,
|
||||
afterData: task,
|
||||
});
|
||||
await this.webhooks.dispatch('task.created', task);
|
||||
if (task.dueDate) {
|
||||
await this.calendarSync.enqueue('TASK', task.id);
|
||||
return this.prisma.$transaction((tx) =>
|
||||
this.tasks.createInTx(
|
||||
tx,
|
||||
{
|
||||
title: dto.title,
|
||||
description: dto.description,
|
||||
priority: dto.priority,
|
||||
dueDate: optionalDate(dto.dueDate),
|
||||
assignedUserId: dto.assignedUserId,
|
||||
relatedType: 'COLONY',
|
||||
relatedId: inspection.colonyId,
|
||||
sourceInspectionId: inspection.id,
|
||||
},
|
||||
actingUserId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private async findIn(db: DbClient, id: string): Promise<Inspection> {
|
||||
const inspection = await db.inspection.findUnique({ where: { id } });
|
||||
if (!inspection) {
|
||||
throw new NotFoundException('Inspection not found');
|
||||
}
|
||||
return task;
|
||||
return inspection;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user