feat: T6 wave 1 – compliance schema, permissions, lock order, backup
Backend Image / build-and-push (push) Successful in 6m39s
Frontend Image / build-and-push (push) Successful in 11m25s

- Migrations (additive): veterinary medicines with lots and movement
  ledger, treatment medicine/lot/dose links, label particulars, authority
  notices, apiary restrictions, compliance settings and acknowledgements;
  notification category COMPLIANCE, added to non-empty Telegram lists.
- Permissions veterinary-medicines:* and compliance:* with role grants.
- LOCKABLE_TABLES: Apiary locks before Colony; T6 tables appended.
- Backup exports and restores the new tables in FK order; unsealed files
  skip them and report dropped medicine links; an open TSK draft in the
  target wins.
- describeNullContracts shared helper; Telegram backlog e2e no longer
  depends on the time of day.

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 13:04:13 +02:00
1 parent 7a1480138d
commit 8a31c42578
23 files changed
+2028 -84

No files matched your search

@@ -113,6 +113,18 @@ describe('deliveryAvailableAt', () => {
expect(deliveryAvailableAt(now, 'SUGGESTION', NO_QUIET)).toEqual(now);
});
it('COMPLIANCE has no time of day: available now, inside quiet hours at their end', () => {
const afternoon = at('2026-10-13', '15:00');
const night = at('2026-10-13', '23:00');
expect(deliveryAvailableAt(afternoon, 'COMPLIANCE', DEFAULTS)).toEqual(
afternoon,
);
expect(deliveryAvailableAt(night, 'COMPLIANCE', DEFAULTS)).toEqual(
at('2026-10-14', '06:30'),
);
});
it('ends quiet hours at 06:30 Berlin time across the clock change', () => {
const now = at('2026-10-24', '22:00');
@@ -0,0 +1,36 @@
import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';
import { NotificationCategory } from '../../../generated/prisma';
import { UpdateNotificationSettingsDto } from './update-notification-settings.dto';
async function errorsOf(body: Record<string, unknown>): Promise<string[]> {
const errors = await validate(
plainToInstance(UpdateNotificationSettingsDto, body),
{ whitelist: true, forbidNonWhitelisted: true },
);
return errors.map((error) => error.property);
}
describe('UpdateNotificationSettingsDto telegramCategories', () => {
it('accepts COMPLIANCE like every other category', async () => {
await expect(
errorsOf({ telegramCategories: ['COMPLIANCE'] }),
).resolves.toEqual([]);
});
it('accepts the complete list of categories, COMPLIANCE included', async () => {
await expect(
errorsOf({ telegramCategories: Object.values(NotificationCategory) }),
).resolves.toEqual([]);
expect(Object.values(NotificationCategory)).toContain('COMPLIANCE');
});
it('rejects an unknown category and a duplicate', async () => {
await expect(
errorsOf({ telegramCategories: ['AUTHORITY'] }),
).resolves.toEqual(['telegramCategories']);
await expect(
errorsOf({ telegramCategories: ['COMPLIANCE', 'COMPLIANCE'] }),
).resolves.toEqual(['telegramCategories']);
});
});
@@ -15,7 +15,7 @@ const stored = {
quietFrom: '21:00' as string | null,
quietUntil: '06:30' as string | null,
telegramEnabled: false,
telegramCategories: ['TASK_DUE', 'DIGEST'],
telegramCategories: ['TASK_DUE', 'DIGEST', 'COMPLIANCE'],
telegramDetail: true,
updatedAt: new Date(0),
};
@@ -167,7 +167,7 @@ describe('NotificationPreferencesService', () => {
digestTime: '07:00',
quietFrom: '21:00',
quietUntil: '06:30',
telegramCategories: ['TASK_DUE', 'DIGEST'],
telegramCategories: ['TASK_DUE', 'DIGEST', 'COMPLIANCE'],
telegramDetail: false,
},
});