fix: T6 browser-check findings (compliance, treatments, labels)
- api-client accepts 2xx answers without body (revoke showed an error) - authority notices: sentOn in the future is rejected (Berlin day) on create, mark-sent and PATCH; dialog defaults to the Berlin day - service worker never caches /api/v1/site-settings - treatments/new no longer overflows at 390 px (min-w-0 on form wrappers); dose with more than 3 decimals is an error, never rounded - medicines: expired lots flagged, disposal over the rest warns - label designer: side panel at lg, sample values instead of placeholders, compliance panel reopens on lg, labelled zoom buttons - skip link to #main-content, headings on no-access pages Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
6c390efe7b
commit
e712c5d4d1
64 files changed
+1497
-323
No files matched your search
@@ -347,6 +347,44 @@ describe('AuthorityNoticesService', () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe('sent day in the future', () => {
|
||||
const FUTURE = '2999-01-01';
|
||||
const MESSAGE = 'Das Sendedatum darf nicht in der Zukunft liegen.';
|
||||
|
||||
it('create rejects it', async () => {
|
||||
await expect(
|
||||
service.create({ kind: 'REGISTRATION', sentOn: FUTURE }, 'u1'),
|
||||
).rejects.toMatchObject({ message: MESSAGE });
|
||||
expect(tx.authorityNotice.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('mark-sent rejects it', async () => {
|
||||
await expect(
|
||||
service.markSent('n1', { sentOn: FUTURE }, 'u1'),
|
||||
).rejects.toMatchObject({ message: MESSAGE });
|
||||
expect(tx.authorityNotice.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('update rejects it', async () => {
|
||||
await expect(
|
||||
service.update('n1', { sentOn: FUTURE }, 'u1'),
|
||||
).rejects.toMatchObject({ message: MESSAGE });
|
||||
expect(tx.authorityNotice.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('accepts today (Berlin day) and clearing with null stays a different error', async () => {
|
||||
jest.useFakeTimers({ now: new Date('2027-03-01T23:30:00.000Z') });
|
||||
try {
|
||||
await service.markSent('n1', { sentOn: '2027-03-02' }, 'u1');
|
||||
await expect(
|
||||
service.markSent('n1', { sentOn: '2027-03-03' }, 'u1'),
|
||||
).rejects.toMatchObject({ message: MESSAGE });
|
||||
} finally {
|
||||
jest.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('mark-sent defaults the sent day to the Berlin day, not the UTC day', async () => {
|
||||
jest.useFakeTimers({ now: new Date('2027-03-01T23:30:00.000Z') });
|
||||
try {
|
||||
@@ -366,7 +404,7 @@ describe('AuthorityNoticesService', () => {
|
||||
it('mark-sent sets SENT, the sent day and drops the open key', async () => {
|
||||
await service.markSent(
|
||||
'n1',
|
||||
{ sentOn: '2027-02-02', sentVia: 'E-Mail' },
|
||||
{ sentOn: '2026-02-02', sentVia: 'E-Mail' },
|
||||
'u1',
|
||||
);
|
||||
|
||||
@@ -374,7 +412,7 @@ describe('AuthorityNoticesService', () => {
|
||||
where: { id: 'n1' },
|
||||
data: expect.objectContaining({
|
||||
status: 'SENT',
|
||||
sentOn: new Date('2027-02-02T00:00:00.000Z'),
|
||||
sentOn: new Date('2026-02-02T00:00:00.000Z'),
|
||||
sentVia: 'E-Mail',
|
||||
openKey: null,
|
||||
}) as unknown,
|
||||
@@ -519,7 +557,7 @@ describe('AuthorityNoticesService', () => {
|
||||
|
||||
it('a draft has no sent day', async () => {
|
||||
await expect(
|
||||
service.update('n1', { sentOn: '2027-01-01' }, 'u1'),
|
||||
service.update('n1', { sentOn: '2026-01-01' }, 'u1'),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
|
||||
@@ -528,7 +566,7 @@ describe('AuthorityNoticesService', () => {
|
||||
notice({ kind: 'REGISTRATION', status: 'SENT', openKey: null }),
|
||||
);
|
||||
|
||||
await service.update('n1', { sentOn: '2027-01-02' }, 'u1');
|
||||
await service.update('n1', { sentOn: '2026-01-02' }, 'u1');
|
||||
|
||||
expect(lockMock.mock.calls.map((call: unknown[]) => call[1])).toEqual([
|
||||
'ComplianceSettings',
|
||||
|
||||
@@ -86,6 +86,17 @@ function assertYearFits(kind: AuthorityNoticeKind, year?: number): void {
|
||||
}
|
||||
}
|
||||
|
||||
/** A notice cannot have been sent tomorrow: sentOn must not lie after the Berlin day. */
|
||||
function futureSentOnError(
|
||||
sentOn: string | null | undefined,
|
||||
): BadRequestException | undefined {
|
||||
return sentOn && sentOn > berlinToday()
|
||||
? new BadRequestException(
|
||||
'Das Sendedatum darf nicht in der Zukunft liegen.',
|
||||
)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AuthorityNoticesService {
|
||||
constructor(
|
||||
@@ -178,6 +189,10 @@ export class AuthorityNoticesService {
|
||||
actingUserId: string,
|
||||
): Promise<AuthorityNotice> {
|
||||
assertYearFits(dto.kind, dto.year);
|
||||
const futureError = futureSentOnError(dto.sentOn);
|
||||
if (futureError) {
|
||||
throw futureError;
|
||||
}
|
||||
const input = {
|
||||
kind: dto.kind,
|
||||
year: dto.year,
|
||||
@@ -234,6 +249,10 @@ export class AuthorityNoticesService {
|
||||
dto: MarkSentDto,
|
||||
actingUserId: string,
|
||||
): Promise<AuthorityNotice> {
|
||||
const futureError = futureSentOnError(dto.sentOn);
|
||||
if (futureError) {
|
||||
return Promise.reject(futureError);
|
||||
}
|
||||
return this.transition(id, actingUserId, { mutex: true }, (_tx, before) => {
|
||||
if (before.status !== 'DRAFT') {
|
||||
return Promise.reject(
|
||||
@@ -290,6 +309,10 @@ export class AuthorityNoticesService {
|
||||
): Promise<AuthorityNotice> {
|
||||
// A new sent day can change which notice is the baseline: the mutex
|
||||
// comes first, in lock order, whenever the sent day is touched.
|
||||
const futureError = futureSentOnError(dto.sentOn);
|
||||
if (futureError) {
|
||||
return Promise.reject(futureError);
|
||||
}
|
||||
const mutex = dto.sentOn !== undefined;
|
||||
return this.transition(id, actingUserId, { mutex }, (_tx, before) => {
|
||||
if (before.status === 'CANCELLED') {
|
||||
|
||||
Reference in new issue
Block a user