fix(agrarmonitor): Karenzzeit vor "Von AM zurück"-Markierung
Build and Push Multi-Platform Images / build-and-push (push) Successful in 39s

Belege, die per E-Mail an Agrarmonitor gesendet werden, brauchen bis zu
10 Minuten bis zum Import in den Dateieingang. Bisher wurden sie im
Upload-Check sofort als "Manuell bearbeiten" + Tag 19 markiert, wenn sie
weder verbucht noch im Dateieingang waren – auch mitten im Import-Fenster.

Jetzt wird vor dem Markieren geprüft, ob die jüngste Notiz mit dem
konfigurierbaren Marker-Text jünger als die Wartezeit ist; falls ja, wird
der Beleg übersprungen und beim nächsten Lauf erneut geprüft. Wartezeit
(agrarmonitor_import_wartezeit_minuten, Standard 10) und Marker
(agrarmonitor_notiz_marker, Standard "Agrarmonitor") sind als Settings
konfigurierbar und in der Settings-Seite editierbar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 12:40:59 +02:00
parent 8fda248683
commit 0765d14d3b
5 changed files with 103 additions and 2 deletions
@@ -56,6 +56,8 @@ export class AgrarmonitorPollingService implements OnModuleInit {
await this.upsertSetting('agrarmonitor_tag_hochgeladen', '');
await this.upsertSetting('agrarmonitor_link_field', '');
await this.upsertSetting('agrarmonitor_tag_manuell', '');
await this.upsertSetting('agrarmonitor_import_wartezeit_minuten', '10');
await this.upsertSetting('agrarmonitor_notiz_marker', 'Agrarmonitor');
}
@Cron(process.env['AGRARMONITOR_POLLING_CRON'] || '0 */30 * * * *')
@@ -80,14 +82,20 @@ export class AgrarmonitorPollingService implements OnModuleInit {
tagHochgeladen: string;
linkField: string;
tagManuell: string;
importWartezeitMinuten: string;
notizMarker: string;
}> {
const [fertig, verbucht, hochgeladen, linkField, manuell] =
const [fertig, verbucht, hochgeladen, linkField, manuell, wartezeit, marker] =
await Promise.all([
this.settingRepo.findOneBy({ Tag: 'agrarmonitor_tag_fertig' }),
this.settingRepo.findOneBy({ Tag: 'agrarmonitor_tag_verbucht' }),
this.settingRepo.findOneBy({ Tag: 'agrarmonitor_tag_hochgeladen' }),
this.settingRepo.findOneBy({ Tag: 'agrarmonitor_link_field' }),
this.settingRepo.findOneBy({ Tag: 'agrarmonitor_tag_manuell' }),
this.settingRepo.findOneBy({
Tag: 'agrarmonitor_import_wartezeit_minuten',
}),
this.settingRepo.findOneBy({ Tag: 'agrarmonitor_notiz_marker' }),
]);
return {
tagFertig: fertig?.Wert ?? '4',
@@ -95,6 +103,8 @@ export class AgrarmonitorPollingService implements OnModuleInit {
tagHochgeladen: hochgeladen?.Wert ?? '',
linkField: linkField?.Wert ?? '',
tagManuell: manuell?.Wert ?? '',
importWartezeitMinuten: wartezeit?.Wert ?? '10',
notizMarker: marker?.Wert ?? 'Agrarmonitor',
};
}
@@ -104,12 +114,16 @@ export class AgrarmonitorPollingService implements OnModuleInit {
tagHochgeladen: string,
linkField: string,
tagManuell: string,
importWartezeitMinuten: string,
notizMarker: string,
): Promise<{
tagFertig: string;
tagVerbucht: string;
tagHochgeladen: string;
linkField: string;
tagManuell: string;
importWartezeitMinuten: string;
notizMarker: string;
}> {
await Promise.all([
this.settingRepo.update(
@@ -132,8 +146,24 @@ export class AgrarmonitorPollingService implements OnModuleInit {
{ Tag: 'agrarmonitor_tag_manuell' },
{ Wert: tagManuell },
),
this.settingRepo.update(
{ Tag: 'agrarmonitor_import_wartezeit_minuten' },
{ Wert: importWartezeitMinuten },
),
this.settingRepo.update(
{ Tag: 'agrarmonitor_notiz_marker' },
{ Wert: notizMarker },
),
]);
return { tagFertig, tagVerbucht, tagHochgeladen, linkField, tagManuell };
return {
tagFertig,
tagVerbucht,
tagHochgeladen,
linkField,
tagManuell,
importWartezeitMinuten,
notizMarker,
};
}
async runPolling(): Promise<PollingResult> {
@@ -393,17 +423,26 @@ export class AgrarmonitorPollingService implements OnModuleInit {
fertigSetting,
linkFieldSetting,
manuellSetting,
wartezeitSetting,
markerSetting,
] = await Promise.all([
this.settingRepo.findOneBy({ Tag: 'agrarmonitor_tag_hochgeladen' }),
this.settingRepo.findOneBy({ Tag: 'agrarmonitor_tag_fertig' }),
this.settingRepo.findOneBy({ Tag: 'agrarmonitor_link_field' }),
this.settingRepo.findOneBy({ Tag: 'agrarmonitor_tag_manuell' }),
this.settingRepo.findOneBy({
Tag: 'agrarmonitor_import_wartezeit_minuten',
}),
this.settingRepo.findOneBy({ Tag: 'agrarmonitor_notiz_marker' }),
]);
const tagHochgeladenId = parseInt(hochgeladenSetting?.Wert ?? '', 10);
const tagFertigId = parseInt(fertigSetting?.Wert ?? '4', 10);
const linkFieldId = parseInt(linkFieldSetting?.Wert ?? '', 10);
const tagManuellId = parseInt(manuellSetting?.Wert ?? '', 10);
const importWartezeitMinuten =
parseInt(wartezeitSetting?.Wert ?? '10', 10) || 10;
const notizMarker = (markerSetting?.Wert ?? 'Agrarmonitor').trim();
if (isNaN(tagHochgeladenId)) {
this.logger.warn(
@@ -512,6 +551,40 @@ export class AgrarmonitorPollingService implements OnModuleInit {
continue;
}
// Karenzzeit: evtl. hat Agrarmonitor die E-Mail noch nicht importiert.
// Ist die Sende-Notiz jünger als die konfigurierte Wartezeit, nicht markieren.
const marker = notizMarker.toLowerCase();
if (marker) {
let notes: Array<{ note: string; created: string }>;
try {
notes = await this.paperlessService.getNotes(doc.id as number);
} catch (err: unknown) {
// Vorsichtig: bei Notiz-Abruf-Fehler NICHT markieren, nächster Lauf prüft erneut
this.logger.warn(
`${interneBelegnummer}: Notiz-Abruf fehlgeschlagen — nicht markiert: ${err instanceof Error ? err.message : err}`,
);
result.skipped++;
await this.delay(500);
continue;
}
const newest = notes
.filter((n) => (n.note ?? '').toLowerCase().includes(marker))
.map((n) => new Date(n.created).getTime())
.filter((t) => !isNaN(t))
.reduce((max, t) => Math.max(max, t), 0);
if (
newest > 0 &&
Date.now() - newest < importWartezeitMinuten * 60 * 1000
) {
this.logger.log(
`${interneBelegnummer}: Sende-Notiz jünger als ${importWartezeitMinuten} Min — warte auf Import`,
);
result.skipped++;
await this.delay(500);
continue;
}
}
// Weder verbucht noch im Dateieingang → Tags "Manuell bearbeiten" + "Von AM zurück" setzen
if (!isNaN(tagManuellId)) {
const currentTags: number[] = (doc.tags as number[]) ?? [];
@@ -42,6 +42,8 @@ export class AgrarmonitorController {
tagHochgeladen: string;
linkField: string;
tagManuell: string;
importWartezeitMinuten: string;
notizMarker: string;
},
) {
return this.pollingService.updatePollingConfig(
@@ -50,6 +52,8 @@ export class AgrarmonitorController {
body.tagHochgeladen,
body.linkField,
body.tagManuell ?? '',
body.importWartezeitMinuten ?? '10',
body.notizMarker ?? 'Agrarmonitor',
);
}
@@ -250,6 +250,14 @@ export class PaperlessService {
return response.data;
}
async getNotes(
id: number,
): Promise<Array<{ id: number; note: string; created: string }>> {
const response = await this.client.get(`/documents/${id}/notes/`);
const data = response.data;
return Array.isArray(data) ? data : (data?.results ?? []);
}
async checksumExists(checksum: string): Promise<boolean> {
const response = await this.client.get('/documents/', {
params: { checksum__iexact: checksum },
+2
View File
@@ -208,6 +208,8 @@ export interface AgrarmonitorPollingConfig {
tagHochgeladen: string;
linkField: string;
tagManuell: string;
importWartezeitMinuten: string;
notizMarker: string;
}
export interface AgrarmonitorPollingResult {
@@ -2600,6 +2600,20 @@ function AgrarmonitorTab() {
))}
</Select>
</Form.Item>
<Form.Item
name="importWartezeitMinuten"
label="Wartezeit bis „zurück&ldquo; (Minuten)"
tooltip="Wie lange nach dem Versand gewartet wird, bevor ein Beleg als „Von Agrarmonitor zurück&ldquo; markiert wird (Import kann bis zu 10 Min dauern)."
>
<Input placeholder="10" style={{ width: 120 }} />
</Form.Item>
<Form.Item
name="notizMarker"
label="Notiz-Marker (Text der AM-Sende-Notiz)"
tooltip="Text, an dem die Sende-Notiz erkannt wird (Teil-Übereinstimmung, Groß-/Kleinschreibung egal). Deren Zeitstempel bestimmt die Wartezeit."
>
<Input placeholder="Agrarmonitor" style={{ width: 280 }} />
</Form.Item>
<Button type="primary" loading={pollingSaving} onClick={handleSavePollingConfig}>
Speichern
</Button>