feat: sync calendar entries to a Microsoft 365 group calendar
Adds a one-way push (ApiCore -> M365) of Aufgaben/Durchsichten/ Behandlungen/Fütterungen/Ernten/Königinnen-Zuordnungen into the calendar of a Microsoft 365 group, via the Graph API. - CalendarSyncModule (app-only client-credentials Graph auth, separate from the interactive SSO app registration; new env vars M365_CALENDAR_TENANT_ID/_CLIENT_ID/_CLIENT_SECRET). - Outbox + minute-interval cron worker (CalendarSyncOutboxEntry) instead of a synchronous push, since Graph calls are too slow/flaky to block a domain create request on; failed syncs retry up to 5 times. - Per-source-type toggle plus a group-ID field, configurable in Verwaltung > Automatisierung > Kalender-Sync (Verwaltung > Backup, an env var still wins on those fields where one applies to related config). - Hooked into Tasks/Inspections/Treatments/Feedings/Harvests/Queens at their existing create (and Task update/complete) points; entries render as all-day Graph events to avoid timezone ambiguity. - Documents the design decision in ARCHITECTURE.md (Annahme 9), since this is a net-new capability the lastenheft doesn't scope. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
28ae0a2723
commit
e7b157ec35
35 files changed
+1760
-7
No files matched your search
@@ -3,6 +3,7 @@ import { QueensService } from './queens.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { AuditLogService } from '../audit/audit-log.service';
|
||||
import { WebhooksService } from '../webhooks/webhooks.service';
|
||||
import { CalendarSyncService } from '../calendar-sync/calendar-sync.service';
|
||||
|
||||
describe('QueensService', () => {
|
||||
let service: QueensService;
|
||||
@@ -48,10 +49,12 @@ describe('QueensService', () => {
|
||||
};
|
||||
auditLog = { record: jest.fn().mockResolvedValue(undefined) };
|
||||
const webhooks = { dispatch: jest.fn().mockResolvedValue(undefined) };
|
||||
const calendarSync = { enqueue: jest.fn(), cancel: jest.fn() };
|
||||
service = new QueensService(
|
||||
prisma as unknown as PrismaService,
|
||||
auditLog as unknown as AuditLogService,
|
||||
webhooks as unknown as WebhooksService,
|
||||
calendarSync as unknown as CalendarSyncService,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -77,7 +80,7 @@ describe('QueensService', () => {
|
||||
currentQueenId: 'queen-old',
|
||||
});
|
||||
tx.colony.update.mockResolvedValue({});
|
||||
tx.queenColonyHistory.create.mockResolvedValue({});
|
||||
tx.queenColonyHistory.create.mockResolvedValue({ id: 'history-1' });
|
||||
|
||||
await service.assignColony('queen-1', { colonyId: 'colony-1' }, 'user-1');
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { AuditLogService } from '../audit/audit-log.service';
|
||||
import { WebhooksService } from '../webhooks/webhooks.service';
|
||||
import { CalendarSyncService } from '../calendar-sync/calendar-sync.service';
|
||||
import { Queen, QueenStatus } from '../../generated/prisma';
|
||||
import { CreateQueenDto } from './dto/create-queen.dto';
|
||||
import { UpdateQueenDto } from './dto/update-queen.dto';
|
||||
@@ -45,6 +46,7 @@ export class QueensService {
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly auditLog: AuditLogService,
|
||||
private readonly webhooks: WebhooksService,
|
||||
private readonly calendarSync: CalendarSyncService,
|
||||
) {}
|
||||
|
||||
async findAll(filter: QueenFilter = {}): Promise<Queen[]> {
|
||||
@@ -118,7 +120,7 @@ export class QueensService {
|
||||
throw new NotFoundException('Colony not found');
|
||||
}
|
||||
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
const history = await this.prisma.$transaction(async (tx) => {
|
||||
if (colony.currentQueenId && colony.currentQueenId !== queen.id) {
|
||||
await tx.queenColonyHistory.updateMany({
|
||||
where: {
|
||||
@@ -137,7 +139,7 @@ export class QueensService {
|
||||
where: { id: colony.id },
|
||||
data: { currentQueenId: queen.id },
|
||||
});
|
||||
await tx.queenColonyHistory.create({
|
||||
return tx.queenColonyHistory.create({
|
||||
data: {
|
||||
queenId: queen.id,
|
||||
colonyId: colony.id,
|
||||
@@ -158,6 +160,7 @@ export class QueensService {
|
||||
|
||||
const updated = await this.findById(id);
|
||||
await this.webhooks.dispatch('queen.replaced', updated);
|
||||
await this.calendarSync.enqueue('QUEEN_ASSIGNMENT', history.id);
|
||||
return updated;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user