diff --git a/paperless-backend/src/agrarmonitor/lieferantennummern.parser.spec.ts b/paperless-backend/src/agrarmonitor/lieferantennummern.parser.spec.ts
index ebb434b..923ccd3 100644
--- a/paperless-backend/src/agrarmonitor/lieferantennummern.parser.spec.ts
+++ b/paperless-backend/src/agrarmonitor/lieferantennummern.parser.spec.ts
@@ -1,5 +1,6 @@
import {
baueFormData,
+ beschreibeSeite,
bestimmeKandidaten,
hoechsteLieferantennummer,
lieferantennummerAusDetail,
@@ -194,3 +195,17 @@ describe('lieferantennummerAusDetail', () => {
);
});
});
+
+describe('beschreibeSeite', () => {
+ it('nennt Titel und Feldnamen, aber keine Werte', () => {
+ const html =
+ '
Anmeldung - AGRARMONITOR' +
+ '';
+ const text = beschreibeSeite(html);
+ expect(text).toContain('Titel „Anmeldung - AGRARMONITOR“');
+ expect(text).toContain('#form_detail fehlt');
+ expect(text).toContain('Felder: email, iban');
+ expect(text).not.toContain('geheim@example.de');
+ expect(text).not.toContain('DE001234');
+ });
+});
diff --git a/paperless-backend/src/agrarmonitor/lieferantennummern.parser.ts b/paperless-backend/src/agrarmonitor/lieferantennummern.parser.ts
index 9be3ee9..ac64f1b 100644
--- a/paperless-backend/src/agrarmonitor/lieferantennummern.parser.ts
+++ b/paperless-backend/src/agrarmonitor/lieferantennummern.parser.ts
@@ -90,6 +90,31 @@ export function lieferantennummerAusDetail(html: string): string {
return (input.attr('value') ?? '').trim();
}
+/**
+ * Kurzbeschreibung einer unerwarteten Seite für das Log: Titel, Formular und
+ * Feldnamen – bewusst ohne Feldwerte (IBAN o. Ä.).
+ */
+export function beschreibeSeite(html: string): string {
+ const $ = cheerio.load(html);
+ const titel = $('title').first().text().trim() || '–';
+ const form = $('form#form_detail');
+ const namen = Array.from(
+ new Set(
+ $('input[name], select[name], textarea[name]')
+ .toArray()
+ .map((el) => $(el).attr('name')!),
+ ),
+ );
+ const feldListe =
+ namen.length > 15
+ ? `${namen.slice(0, 15).join(', ')}, …`
+ : namen.join(', ');
+ return (
+ `Titel „${titel}“, #form_detail ${form.length ? 'vorhanden' : 'fehlt'}, ` +
+ `${html.length} Zeichen, Felder: ${feldListe || 'keine'}`
+ );
+}
+
function istDisabled(el: Cheerio): boolean {
return el.attr('disabled') !== undefined;
}
diff --git a/paperless-backend/src/agrarmonitor/lieferantennummern.service.spec.ts b/paperless-backend/src/agrarmonitor/lieferantennummern.service.spec.ts
index 43d381a..c07d8e4 100644
--- a/paperless-backend/src/agrarmonitor/lieferantennummern.service.spec.ts
+++ b/paperless-backend/src/agrarmonitor/lieferantennummern.service.spec.ts
@@ -53,6 +53,7 @@ function fakeHttp(opts: {
nummern: Record;
dubletten?: number[];
speichernIgnorieren?: number[];
+ detailHtml?: string;
}) {
const aufrufe: string[] = [];
const updates: UpdateBody[] = [];
@@ -62,7 +63,8 @@ function fakeHttp(opts: {
const m = /\/kunden\/detail\/(\d+)/.exec(url);
if (m) {
return Promise.resolve({
- data: detail(opts.nummern[Number(m[1])] ?? ''),
+ status: 200,
+ data: opts.detailHtml ?? detail(opts.nummern[Number(m[1])] ?? ''),
});
}
return Promise.resolve({ data: '' });
@@ -148,4 +150,23 @@ describe('LieferantennummernService.vergeben', () => {
expect(lauf.eintraege[0].hinweis).toContain('15550706');
expect(aufrufe[aufrufe.length - 1]).toContain('itemsperpage=25');
});
+
+ it('lädt die Detailseite ohne AJAX-Header und meldet eine unerwartete Seite verständlich', async () => {
+ const { http, updates } = fakeHttp({
+ nummern: {},
+ detailHtml:
+ 'Anmeldung - AGRARMONITOR',
+ });
+ const lauf = neuerLauf(1);
+ await service().vergeben(http, [kandidat(1, 'A')], 15550706, lauf);
+
+ const detailAufruf = (http.get as jest.Mock).mock.calls.find(
+ ([url]) => url === '/kunden/detail/1',
+ ) as [string, { headers?: Record }];
+ expect(detailAufruf[1].headers?.['X-Requested-With']).toBeUndefined();
+ expect(updates).toHaveLength(0);
+ expect(lauf).toMatchObject({ fehler: 1, abgebrochen: true });
+ expect(lauf.eintraege[0].hinweis).toContain('HTTP 200');
+ expect(lauf.eintraege[0].hinweis).toContain('Anmeldung - AGRARMONITOR');
+ });
});
diff --git a/paperless-backend/src/agrarmonitor/lieferantennummern.service.ts b/paperless-backend/src/agrarmonitor/lieferantennummern.service.ts
index 9c165b7..a59fb5d 100644
--- a/paperless-backend/src/agrarmonitor/lieferantennummern.service.ts
+++ b/paperless-backend/src/agrarmonitor/lieferantennummern.service.ts
@@ -10,6 +10,7 @@ import { fehlerText } from './agrarmonitor-polling.service';
import {
anzeigeName,
baueFormData,
+ beschreibeSeite,
bestimmeKandidaten,
hoechsteLieferantennummer,
lieferantennummerAusDetail,
@@ -318,15 +319,29 @@ export class LieferantennummernService {
return { maximum, kandidaten: bestimmeKandidaten(lieferanten) };
}
+ /**
+ * Lädt die Detailseite wie eine normale Browser-Navigation (ohne AJAX-Header)
+ * und prüft, dass das Formular mit #lieferantennummer enthalten ist.
+ */
private async ladeDetail(
http: AxiosInstance,
kundeId: number,
): Promise {
- const response = await http.get(`/kunden/detail/${kundeId}`, {
- headers: AJAX_HEADERS,
- responseType: 'text',
- });
- return response.data;
+ const url = `/kunden/detail/${kundeId}`;
+ const response = await http.get(url, { responseType: 'text' });
+ const html = String(response.data ?? '');
+ try {
+ lieferantennummerAusDetail(html);
+ } catch {
+ const request = response.request as
+ | { res?: { responseUrl?: string } }
+ | undefined;
+ const zielUrl = request?.res?.responseUrl ?? url;
+ throw new Error(
+ `Detailseite ohne Feld #lieferantennummer (HTTP ${response.status}, ${zielUrl}): ${beschreibeSeite(html)}`,
+ );
+ }
+ return html;
}
/** Dublettencheck wie in der Weboberfläche; true, wenn Agrarmonitor ähnliche Kontakte meldet. */