feat: T6 wave 4 – notice PDFs, compliance notifications, readiness core
- 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:
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
|
||||
|
||||
Reference in new issue
Block a user