feat: T4 wave 10 – bottling quality, phenology and honey chain UI, Telegram link/receive and ops

- Bottling quality: water content above the legal limit (strict >) answers
  409 CONFIRMATION_REQUIRED bound to the measured value; prefill for jar
  size, best-before (shelfLifeMonths, month-end clamping) and water content;
  impossible dates give 400; water-content checks in src/common/quality.
- Frontend phenology: "Entwicklung melden" sheet, IndexedDB v4 queue,
  season header on the apiary page, confirmed revoke for all apiaries.
- Frontend honey chain: "Vorschlag aus …" hints with explicit take-over,
  prefill status note, movement ledger with withdraw/correct dialogs.
- Telegram: link tokens (hashed, single use), private-chat receiver with
  lease and offset CAS, bounded poison-update skip, Retry-After, pending
  deliveries skipped on disconnect; system API for deliveries and retry;
  worker-only secret wiring and manifest policy tests.
- ARCHITECTURE.md §7, §9, §10, §11.11–11.13, Annahmen 51–55.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-14 22:47:17 +02:00
1 parent 16cc557462
commit fab7f7e12f
91 files changed
+8463 -104

No files matched your search

@@ -31,6 +31,7 @@ describe('NotificationPreferencesService', () => {
};
let prisma: {
notificationPreference: { upsert: jest.Mock };
telegramLink: { findUnique: jest.Mock };
$transaction: jest.Mock;
};
const order: string[] = [];
@@ -62,6 +63,7 @@ describe('NotificationPreferencesService', () => {
notificationPreference: {
upsert: jest.fn().mockResolvedValue(stored),
},
telegramLink: { findUnique: jest.fn().mockResolvedValue(null) },
$transaction: jest
.fn()
.mockImplementation((work: (t: unknown) => Promise<unknown>) =>
@@ -180,4 +182,75 @@ describe('NotificationPreferencesService', () => {
expect(arg.data).not.toHaveProperty('telegramEnabled');
expect(arg.data.telegramCategories).toEqual(['SYSTEM']);
});
describe('toView (Telegram status)', () => {
const originalUsername = process.env.TELEGRAM_BOT_USERNAME;
const originalToken = process.env.TELEGRAM_BOT_TOKEN;
afterEach(() => {
if (originalUsername === undefined) {
delete process.env.TELEGRAM_BOT_USERNAME;
} else {
process.env.TELEGRAM_BOT_USERNAME = originalUsername;
}
if (originalToken === undefined) {
delete process.env.TELEGRAM_BOT_TOKEN;
} else {
process.env.TELEGRAM_BOT_TOKEN = originalToken;
}
});
it('NotificationSettingsView.telegram.configured is true with the username set and no token in the API process', async () => {
process.env.TELEGRAM_BOT_USERNAME = '@apicore_test_bot';
delete process.env.TELEGRAM_BOT_TOKEN;
const view = await service.toView(stored as never);
expect(view.telegram).toEqual({
configured: true,
botUsername: 'apicore_test_bot',
linked: false,
firstName: null,
linkedAt: null,
enabled: false,
});
expect(JSON.stringify(view)).not.toMatch(/token/i);
});
it('is not configured without a bot username, even with a token', async () => {
process.env.TELEGRAM_BOT_USERNAME = '';
process.env.TELEGRAM_BOT_TOKEN =
'123456789:AAEhBOweik6ad9r_QXMENQjcrGbqCr4K-RM';
const view = await service.toView(stored as never);
expect(view.telegram.configured).toBe(false);
expect(view.telegram.botUsername).toBeNull();
});
it('shows the link without the chat id', async () => {
process.env.TELEGRAM_BOT_USERNAME = 'apicore_test_bot';
prisma.telegramLink.findUnique.mockResolvedValue({
userId: 'u1',
chatId: 4_711_000_001n,
telegramFirstName: 'Anna',
linkedAt: new Date('2026-10-14T08:00:00.000Z'),
});
const view = await service.toView({
...stored,
telegramEnabled: true,
} as never);
expect(view.telegram).toMatchObject({
linked: true,
firstName: 'Anna',
linkedAt: '2026-10-14T08:00:00.000Z',
enabled: true,
});
expect(JSON.stringify(view)).not.toContain('4711000001');
expect(prisma.telegramLink.findUnique).toHaveBeenCalledWith({
where: { userId: 'u1' },
});
});
});
});