feat(agrarmonitor): fehlende Lieferantennummern vergeben
Build and Push Multi-Platform Images / build-and-push (push) Successful in 1m1s

Neuer Bereich „Lieferantennummern überprüfen“ in den Agrarmonitor-
Einstellungen. Er vergibt allen aktiven Lieferanten ohne
Lieferantennummer eine fortlaufende Nummer im Schema 1555xxxx, ab der
höchsten vergebenen Nummer (Inaktive zählen mit, Altlasten werden
ignoriert).

- Vorschau „Lieferant → neue Nummer“, geschrieben wird erst nach
  Bestätigung; Einzelmodus zum Testen an einem Lieferanten
- formData wird wie in der Weboberfläche aus #form_detail aufgebaut,
  nur die Lieferantennummer ändert sich
- Dublettencheck über vorhanden.php, Erfolgskontrolle durch Neuladen
  des Details, Abbruch beim ersten Fehler
- Lauf im Hintergrund mit Status-Abfrage (wegen Proxy-Timeouts),
  Listenfilter werden danach zurückgesetzt
- HTML-Parsing mit cheerio, Unit-Tests für Parser und Vergabe-Ablauf

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bjoernpoettkerandClaude Opus 5.5 committed 2026-09-28 08:51:13 +02:00
1 parent 029009182a
commit 3d4125b29a
11 files changed
+1507 -3

No files matched your search

@@ -0,0 +1,151 @@
import type { AxiosInstance } from 'axios';
import type { AgrarmonitorService } from './agrarmonitor.service';
import {
LieferantennummernService,
type LieferantennummerLaufErgebnis,
} from './lieferantennummern.service';
import type { ListenLieferant } from './lieferantennummern.parser';
// Der echte Connector lädt jsdom (ESM), das Jest nicht verarbeiten kann – hier wird er nicht gebraucht.
jest.mock('agrarmonitor-connector', () => ({}));
interface UpdateBody {
kundeID: number;
formData: Record<string, unknown>;
}
function detail(nummer: string): string {
return (
'<form id="form_detail">' +
'<input type="text" name="kundennummer" id="kundennummer" value="21207">' +
`<input type="text" name="lieferantennummer" id="lieferantennummer" value="${nummer}">` +
'<input type="text" name="firma" value="Firma">' +
'<input type="checkbox" name="ist_kunde" value="1">' +
'<input type="checkbox" name="ist_lieferant" value="1" checked>' +
'</form>'
);
}
function kandidat(kundeId: number, firma: string): ListenLieferant {
return { kundeId, lieferantennummer: '', firma, name: '', aktiv: true };
}
function neuerLauf(gesamt: number): LieferantennummerLaufErgebnis {
return {
modus: 'alle',
gestartetAm: '',
beendetAm: null,
gesamt,
vergeben: 0,
uebersprungen: 0,
zuPruefen: 0,
fehler: 0,
abgebrochen: false,
eintraege: [],
};
}
/**
* Simuliert Agrarmonitor: speichert Nummern aus updateKunde.php pro Kunde,
* vorhanden.php meldet für die Ids in `dubletten` ähnliche Kontakte.
*/
function fakeHttp(opts: {
nummern: Record<number, string>;
dubletten?: number[];
speichernIgnorieren?: number[];
}) {
const aufrufe: string[] = [];
const updates: UpdateBody[] = [];
const http = {
get: jest.fn((url: string) => {
aufrufe.push(`GET ${url}`);
const m = /\/kunden\/detail\/(\d+)/.exec(url);
if (m) {
return Promise.resolve({
data: detail(opts.nummern[Number(m[1])] ?? ''),
});
}
return Promise.resolve({ data: '' });
}),
post: jest.fn((url: string, body: string | UpdateBody) => {
aufrufe.push(`POST ${url}`);
if (typeof body === 'string') {
const id = Number(new URLSearchParams(body).get('customerID'));
return Promise.resolve({
data: opts.dubletten?.includes(id) ? '<div>Ähnlich</div>' : '',
});
}
updates.push(body);
if (!opts.speichernIgnorieren?.includes(body.kundeID)) {
opts.nummern[body.kundeID] = String(body.formData.lieferantennummer);
}
return Promise.resolve({ data: '/kunden/detail/' + body.kundeID });
}),
};
return { http: http as unknown as AxiosInstance, aufrufe, updates };
}
function service() {
const agrarmonitor = { clearClient: jest.fn() };
return new LieferantennummernService(
agrarmonitor as unknown as AgrarmonitorService,
);
}
describe('LieferantennummernService.vergeben', () => {
it('vergibt fortlaufend, überspringt Vorhandene und verbraucht bei Dubletten keine Nummer', async () => {
const { http, updates, aufrufe } = fakeHttp({
nummern: { 2: '15550010' },
dubletten: [3],
});
const lauf = neuerLauf(4);
await service().vergeben(
http,
[kandidat(1, 'A'), kandidat(2, 'B'), kandidat(3, 'C'), kandidat(4, 'D')],
15550706,
lauf,
);
expect(lauf.eintraege.map((e) => [e.kundeId, e.status, e.nummer])).toEqual([
[1, 'vergeben', '15550706'],
[2, 'uebersprungen', undefined],
[3, 'pruefen', undefined],
[4, 'vergeben', '15550707'],
]);
expect(lauf).toMatchObject({
vergeben: 2,
uebersprungen: 1,
zuPruefen: 1,
fehler: 0,
abgebrochen: false,
});
expect(updates[0].kundeID).toBe(1);
expect(updates[0].formData).toMatchObject({
kundennummer: '21207',
lieferantennummer: '15550706',
ist_kunde: '',
ist_lieferant: '1',
});
expect(aufrufe[aufrufe.length - 1]).toContain('itemsperpage=25');
expect(lauf.beendetAm).not.toBeNull();
});
it('bricht ab, wenn die Nummer nach dem Speichern nicht übernommen wurde', async () => {
const { http, updates, aufrufe } = fakeHttp({
nummern: {},
speichernIgnorieren: [1],
});
const lauf = neuerLauf(2);
await service().vergeben(
http,
[kandidat(1, 'A'), kandidat(2, 'B')],
15550706,
lauf,
);
expect(updates).toHaveLength(1);
expect(lauf).toMatchObject({ vergeben: 0, fehler: 1, abgebrochen: true });
expect(lauf.eintraege[0].hinweis).toContain('15550706');
expect(aufrufe[aufrufe.length - 1]).toContain('itemsperpage=25');
});
});