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:
1 parent
16cc557462
commit
fab7f7e12f
91 files changed
+8463
-104
No files matched your search
@@ -7,7 +7,11 @@ import { AppModule } from '../../src/app.module';
|
||||
import { configureApp } from '../../src/app.config';
|
||||
import { THROTTLER_NAMESPACE } from '../../src/common/throttler/prisma-throttler.storage';
|
||||
import { TelegramApiError } from '../../src/telegram/telegram-api-error';
|
||||
import { TelegramClient } from '../../src/telegram/telegram-client';
|
||||
import {
|
||||
TelegramClient,
|
||||
type TelegramUpdate,
|
||||
type TelegramUpdateBatch,
|
||||
} from '../../src/telegram/telegram-client';
|
||||
import { TelegramModule } from '../../src/telegram/telegram.module';
|
||||
|
||||
export interface SentMessage {
|
||||
@@ -23,6 +27,12 @@ export interface SentMessage {
|
||||
export class FakeTelegramClient {
|
||||
readonly sent: SentMessage[] = [];
|
||||
private readonly failures: { chatId: bigint | null; error: Error }[] = [];
|
||||
private readonly updates: TelegramUpdate[] = [];
|
||||
/** Every offset getUpdates was asked for, in order. */
|
||||
readonly polledOffsets: bigint[] = [];
|
||||
/** Runs inside getUpdates, after the updates are chosen (e.g. steal the lease). */
|
||||
onGetUpdates: (() => Promise<void>) | null = null;
|
||||
private getUpdatesError: Error | null = null;
|
||||
|
||||
isConfigured(): boolean {
|
||||
return true;
|
||||
@@ -47,6 +57,34 @@ export class FakeTelegramClient {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/** What the next getUpdates calls return (from `offset` on, like Telegram). */
|
||||
queueUpdate(update: TelegramUpdate): void {
|
||||
this.updates.push(update);
|
||||
}
|
||||
|
||||
failNextGetUpdates(error: Error): void {
|
||||
this.getUpdatesError = error;
|
||||
}
|
||||
|
||||
async getUpdates(offset: bigint): Promise<TelegramUpdateBatch> {
|
||||
this.polledOffsets.push(offset);
|
||||
if (this.getUpdatesError !== null) {
|
||||
const error = this.getUpdatesError;
|
||||
this.getUpdatesError = null;
|
||||
throw error;
|
||||
}
|
||||
const due = this.updates.filter((update) => update.updateId >= offset);
|
||||
await this.onGetUpdates?.();
|
||||
const highestUpdateId = due.reduce<bigint | null>(
|
||||
(highest, update) =>
|
||||
highest === null || update.updateId > highest
|
||||
? update.updateId
|
||||
: highest,
|
||||
null,
|
||||
);
|
||||
return { updates: due, highestUpdateId };
|
||||
}
|
||||
|
||||
sentTo(chatId: bigint): SentMessage[] {
|
||||
return this.sent.filter((message) => message.chatId === chatId);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user