feat: T6 wave 4 – notice PDFs, compliance notifications, readiness core
Backend Image / build-and-push (push) Successful in 4m58s
Frontend Image / build-and-push (push) Successful in 7m57s

- Authority notice PDFs: pure model plus renderer, TSK count in A4
  landscape, FEHLT for missing particulars, stored totals with correction
  note; GET /authority-notices/:id/pdf with compliance:read.
- Compliance notifications in category COMPLIANCE: herd change and TSK
  reference day, at most one unread per topic and user (advisory lock);
  daily rule TskReferenceDayReached with Berlin day and catch-up; handlers
  in API and worker.
- Readiness core: live evaluation of seven pure rules, fingerprinted
  acknowledgements that reopen when revoked or stale, subject masking,
  read-only GET paths; READINESS_RULES injectable; kept out of the worker.
- e2e hygiene: no unscoped deletes, seeded settings rows saved/restored.
- ARCHITECTURE.md §12.8–12.10, §10 handler rows, Annahmen 78–85.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-15 14:48:57 +02:00
1 parent edc193f128
commit b912d29166
54 files changed
+5300 -14

No files matched your search

@@ -18,7 +18,13 @@ import { CreateAuthorityNoticeDto } from './dto/create-authority-notice.dto';
import { MarkSentDto } from './dto/mark-sent.dto';
import { UpdateAuthorityNoticeDto } from './dto/update-authority-notice.dto';
import { buildHerdSnapshot, loadHerdApiaryRows } from './herd-snapshot';
import { COMPLIANCE_SETTINGS_ID, findBaselineIn } from './herd-status';
import {
COMPLIANCE_SETTINGS_ID,
complianceSettingsIn,
findBaselineIn,
} from './herd-status';
import { buildNoticePdfModel } from './pdf/authority-notice-model';
import { renderAuthorityNoticePdf } from './pdf/authority-notice-pdf';
const AUDIT_ENTITY = 'AuthorityNotice';
const LIST_LIMIT = 200;
@@ -108,6 +114,60 @@ export class AuthorityNoticesService {
return notice;
}
/**
* The PDF of a notice. A change notice compares with the notice that was
* the baseline when it was (or would be) sent. The file name carries the
* id only, never user input.
*/
async pdf(id: string): Promise<{ buffer: Buffer; fileName: string }> {
const notice = await this.findOne(id);
const [site, settings, baseline] = await Promise.all([
this.prisma.siteSettings.findUnique({ where: { id: 'singleton' } }),
complianceSettingsIn(this.prisma),
notice.kind === 'HERD_CHANGE'
? this.baselineFor(notice)
: Promise.resolve(null),
]);
const model = buildNoticePdfModel({
notice,
baseline,
site: {
holderName: site?.holderName ?? null,
holderAddress: site?.holderAddress ?? null,
},
settings,
now: new Date(),
});
return {
buffer: await renderAuthorityNoticePdf(model),
fileName: `anzeige-${notice.id}.pdf`,
};
}
/** A sent notice compares with the one sent before it, a draft with the newest. */
private baselineFor(
notice: AuthorityNotice,
): Promise<AuthorityNotice | null> {
if (notice.status !== 'SENT' || !notice.sentOn) {
return findBaselineIn(this.prisma);
}
return this.prisma.authorityNotice.findFirst({
where: {
status: 'SENT',
kind: { in: ['REGISTRATION', 'HERD_CHANGE'] },
id: { not: notice.id },
OR: [
{ sentOn: { lt: notice.sentOn } },
{
sentOn: notice.sentOn,
createdAt: { lt: notice.createdAt },
},
],
},
orderBy: [{ sentOn: 'desc' }, { createdAt: 'desc' }, { id: 'desc' }],
});
}
/**
* Creates a draft, or a SENT notice at once when `sentOn` is given (the
* way to record what was already reported). A second open draft of the