Add invoice and customer API methods

This commit is contained in:
2026-05-23 11:17:05 +02:00
parent 921c67503b
commit cf6bc1b5cc
9 changed files with 756 additions and 24 deletions
+275 -8
View File
@@ -11,6 +11,7 @@ const tough_cookie_1 = require("tough-cookie");
class AgrarmonitorConnector {
options;
http;
static s3DateienBaseUrl = 'https://s3-eu-central-1.amazonaws.com/dateien.agrarmonitor.de/07';
baseUrl;
apiBaseUrl;
timeoutMs;
@@ -19,11 +20,12 @@ class AgrarmonitorConnector {
loginStrategy;
logger;
cookieJar;
apiHttp;
loginInProgress = null;
constructor(options) {
this.options = options;
this.baseUrl = options.baseUrl ?? 'https://admin7.agrarmonitor.de';
this.apiBaseUrl = options.apiBaseUrl ?? 'https://api.agrarmonitor.de';
this.apiBaseUrl = this.normalizeApiBaseUrl(options.apiBaseUrl ?? 'https://api.agrarmonitor.de/v1');
this.timeoutMs = options.timeoutMs ?? 15000;
this.autoLogin = options.autoLogin ?? true;
this.autoRetry = options.autoRetry ?? true;
@@ -33,6 +35,7 @@ class AgrarmonitorConnector {
async init() {
this.cookieJar = await this.options.cookieStore.load();
this.http = this.createHttpClient();
this.apiHttp = this.createApiHttpClient();
if (this.autoLogin) {
const valid = await this.isSessionValid();
if (!valid) {
@@ -54,6 +57,7 @@ class AgrarmonitorConnector {
this.cookieJar = new tough_cookie_1.CookieJar();
await this.options.cookieStore.clear();
this.http = this.createHttpClient();
this.apiHttp = this.createApiHttpClient();
}
async saveSession() {
await this.options.cookieStore.save(this.cookieJar);
@@ -129,23 +133,136 @@ class AgrarmonitorConnector {
};
}
async fetchCustomers(options = {}) {
const apiToken = options.apiToken ?? this.options.apiToken;
if (!apiToken) {
throw new Error('Agrarmonitor API-Token nicht konfiguriert');
}
const response = await this.http.get(`${this.apiBaseUrl}/v1/kunden`, {
const response = await this.apiRequest('/kunden', {
params: {
per_page: options.perPage ?? 99999,
api_token: apiToken,
},
apiToken: options.apiToken,
});
await this.saveSession();
const responseData = response.data;
if (!responseData || !Array.isArray(responseData.data)) {
throw new Error('Ungueltige Agrarmonitor API-Antwort');
}
return responseData.data;
}
async eingangsrechnungenLivesearch(suchstring) {
const response = await this.http.get('/module/dateien/livesearch.php', {
params: this.createDateienLivesearchParams(suchstring),
});
await this.saveSession();
const document = this.parseHtmlDocument(response.data);
const rows = Array.from(document.querySelectorAll('table#dateien tbody tr'));
const results = [];
for (const row of rows) {
const cells = Array.from(row.querySelectorAll('td'));
const typText = cells[3]?.textContent?.trim() ?? '';
if (!typText.startsWith('Eingangsrechnungen')) {
continue;
}
const dokumentId = this.parseNumber(row.getAttribute('data-file_id'));
const dataFile = row.getAttribute('data-file') ?? '';
const dokumentName = cells[2]?.querySelector('b > a')?.textContent?.trim() ?? '';
const dateiName = cells[2]?.querySelector('span')?.textContent?.trim() ?? '';
const belegLink = cells[3]?.querySelector('a');
const belegTextParts = (belegLink?.textContent ?? '').split(',').map(part => part.trim()).filter(Boolean);
const belegNummer = belegTextParts[0] ?? '';
const belegDatum = this.parseGermanShortDateFromText(belegTextParts.at(-1) ?? '');
const eingangId = this.parseNumber(this.lastPathSegment(belegLink?.getAttribute('href') ?? ''));
const { interneBelegNummer, kundenId, betriebId, dokumentTyp } = await this.getEingangsrechnungEditMeta(eingangId);
const { eingangsDatum, buchungsDatum } = await this.getEingangsrechnungDetailMeta(eingangId);
results.push({
dokumentId,
vorschauUrl: `${AgrarmonitorConnector.s3DateienBaseUrl}/v_${this.fileBasename(dataFile)}.png`,
dokumentUrl: `${AgrarmonitorConnector.s3DateienBaseUrl}/${dataFile}`,
dokumentName,
dateiName,
belegNummer,
interneBelegNummer,
belegDatum,
buchungsDatum,
eingangsDatum,
eingangId,
kundenId,
betriebId,
dokumentTyp,
});
}
return results;
}
async eingangsrechnungVorhanden(suchstring) {
const response = await this.http.get('/module/dateien/livesearch.php', {
params: this.createDateienLivesearchParams(suchstring),
});
await this.saveSession();
return this.hasTableRows(response.data, 'table#dateien tbody tr');
}
async eingangsrechnungImDateieingangVorhanden(suchstring) {
const response = await this.http.get('/module/dateien/eingang/livesearch.php', {
params: {
suchstring,
seite: 1,
},
});
await this.saveSession();
return this.hasTableRows(response.data, 'table#dateien_eingang tbody tr');
}
async getRechnungsdaten(rechnungId) {
const response = await this.http.get('/module/eingangsrechnungen/api/eingangsrechnungen.php', {
params: {
id: 'edit',
rechnungId,
},
});
await this.saveSession();
const document = this.parseHtmlDocument(response.data);
return {
lieferschein: this.inputValue(document, 'lieferscheinnummer'),
rechnung: this.inputValue(document, 'rechnungsnummer'),
datum: this.requireDate(this.parseGermanShortDate(this.inputValue(document, 'rechnungsdatum')), 'rechnungsdatum'),
kundenId: this.selectedNumberValue(document, 'rgempf'),
adresstext: this.inputValue(document, 'addressName'),
};
}
async setRechnungsdaten(rechnungId, daten) {
const response = await this.http.post(`/module/eingangsrechnungen/api/eingangsrechnungen.php?id=update&rechnungId=${encodeURIComponent(rechnungId)}`, new URLSearchParams({
lieferscheinnummer: daten.lieferschein,
rechnungsnummer: daten.rechnung,
rechnungsdatum: this.formatGermanShortDate(daten.datum),
rgempf: String(daten.kundenId),
adresstext: daten.adresstext,
}), this.formPostConfig(`/eingangsrechnungen/detail/${rechnungId}`));
await this.saveSession();
return response.status >= 200 && response.status < 300;
}
async setLieferscheinNummer(rechnungId, nummer) {
const rechnungsdaten = await this.getRechnungsdaten(rechnungId);
const success = await this.setRechnungsdaten(rechnungId, {
...rechnungsdaten,
lieferschein: nummer,
});
if (!success) {
throw new Error('Lieferscheinnummer konnte nicht gespeichert werden');
}
}
async setEingangsdatum(rechnungId, datum) {
const response = await this.http.post('/module/eingangsrechnungen/api/updateReceived.php', new URLSearchParams({
datum: this.formatGermanShortDate(datum),
receiptID: String(rechnungId),
}), this.formPostConfig(`/eingangsrechnungen/detail/${rechnungId}`));
await this.saveSession();
return response.status >= 200 && response.status < 300;
}
async getCustomerById(id) {
const response = await this.apiRequest(`/kunden/${id}`);
this.logDebug('Agrarmonitor customer API raw response', response.data);
if (this.isWrappedApiCustomer(response.data)) {
return response.data.data;
}
if (this.isApiCustomer(response.data)) {
return response.data;
}
throw new Error('Ungueltige Agrarmonitor Kunden-API-Antwort');
}
createHttpClient() {
const client = (0, axios_cookiejar_support_1.wrapper)(axios_1.default.create({
baseURL: this.baseUrl,
@@ -173,6 +290,26 @@ class AgrarmonitorConnector {
});
return client;
}
createApiHttpClient(apiToken = this.options.apiToken) {
return axios_1.default.create({
baseURL: this.apiBaseUrl,
timeout: this.timeoutMs,
headers: {
Accept: 'application/json',
...(apiToken ? { Authorization: `Bearer ${apiToken}` } : {}),
},
validateStatus: status => status >= 200 && status < 500,
});
}
async apiRequest(url, config = {}) {
const apiToken = config.apiToken ?? this.options.apiToken;
if (!apiToken) {
throw new Error('Agrarmonitor API-Token nicht konfiguriert');
}
const { apiToken: _apiToken, ...axiosConfig } = config;
const client = apiToken === this.options.apiToken ? this.apiHttp : this.createApiHttpClient(apiToken);
return client.get(url, axiosConfig);
}
async performLogin() {
if (!this.options.username || !this.options.password) {
throw new Error('Agrarmonitor-Credentials nicht konfiguriert');
@@ -267,6 +404,136 @@ class AgrarmonitorConnector {
await this.login();
return this.http.request(config);
}
createDateienLivesearchParams(suchstring) {
return {
suchstring,
stammdatum_typ: -1,
mobil: -1,
sensibel: -1,
firma: 0,
itemsperpage: 100000,
seite: 1,
};
}
async getEingangsrechnungEditMeta(rechnungId) {
const response = await this.http.get('/module/eingangsrechnungen/api/eingangsrechnungen.php', {
params: {
id: 'edit',
rechnungId,
},
});
await this.saveSession();
const document = this.parseHtmlDocument(response.data);
return {
interneBelegNummer: this.inputValue(document, 'lieferscheinnummer'),
kundenId: this.selectedNumberValue(document, 'rgempf'),
betriebId: this.selectedNumberValue(document, 'firma_id'),
dokumentTyp: this.selectedNumberValue(document, 'typ'),
};
}
async getEingangsrechnungDetailMeta(rechnungId) {
const response = await this.http.get(`/eingangsrechnungen/detail/${rechnungId}`);
await this.saveSession();
const document = this.parseHtmlDocument(response.data);
const receivedStatus = document.querySelector('#receivedStatus');
const receivedText = receivedStatus?.textContent?.trim() ?? '';
const parentParts = (receivedStatus?.parentElement?.textContent ?? '').split('-');
const bookingText = parentParts.at(-1)?.trim() ?? '';
return {
eingangsDatum: !receivedText || receivedText === 'Nicht empfangen'
? null
: this.parseGermanShortDateFromText(receivedText.slice(13).trim()),
buchungsDatum: !bookingText || bookingText === 'Nicht gebucht'
? null
: this.parseGermanShortDateFromText(bookingText.slice(11).trim()),
};
}
formPostConfig(refererPath) {
return {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Referer: `${this.baseUrl}${refererPath}`,
Origin: this.baseUrl,
},
validateStatus: status => status >= 200 && status < 400,
};
}
parseHtmlDocument(data) {
return new jsdom_1.JSDOM(typeof data === 'string' ? data : String(data ?? '')).window.document;
}
hasTableRows(data, selector) {
return this.parseHtmlDocument(data).querySelectorAll(selector).length > 0;
}
inputValue(document, name) {
return document.querySelector(`input[name="${name}"]`)?.value.trim() ?? '';
}
selectedNumberValue(document, name) {
return this.parseNumber(document.querySelector(`select[name="${name}"] option:checked`)?.value);
}
parseNumber(value) {
const numberValue = Number(String(value ?? '').trim());
return Number.isFinite(numberValue) ? numberValue : 0;
}
parseGermanShortDate(value) {
const match = value.trim().match(/^(\d{2})\.(\d{2})\.(\d{2})$/);
if (!match) {
return null;
}
const [, day, month, year] = match;
const parsed = new Date(Number(`20${year}`), Number(month) - 1, Number(day));
if (parsed.getFullYear() !== Number(`20${year}`) ||
parsed.getMonth() !== Number(month) - 1 ||
parsed.getDate() !== Number(day)) {
return null;
}
return parsed;
}
requireDate(value, fieldName) {
if (!value) {
throw new Error(`Ungueltiges Datumsformat fuer ${fieldName}`);
}
return value;
}
formatGermanShortDate(date) {
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = String(date.getFullYear()).slice(-2);
return `${day}.${month}.${year}`;
}
lastPathSegment(value) {
return value.split('?')[0]?.split('/').filter(Boolean).at(-1) ?? '';
}
fileBasename(fileName) {
const lastDotIndex = fileName.lastIndexOf('.');
return lastDotIndex === -1 ? fileName : fileName.slice(0, lastDotIndex);
}
normalizeApiBaseUrl(value) {
const withoutTrailingSlash = value.replace(/\/+$/, '');
return withoutTrailingSlash.endsWith('/v1') ? withoutTrailingSlash : `${withoutTrailingSlash}/v1`;
}
isWrappedApiCustomer(value) {
return (typeof value === 'object' &&
value !== null &&
'data' in value &&
this.isApiCustomer(value.data));
}
isApiCustomer(value) {
return (typeof value === 'object' &&
value !== null &&
'id' in value &&
(typeof value.id === 'string' || typeof value.id === 'number'));
}
parseGermanShortDateFromText(value) {
const match = value.match(/(\d{2}\.\d{2}\.\d{2})/);
return match ? this.parseGermanShortDate(match[1]) : null;
}
logDebug(message, meta) {
if (this.logger?.debug) {
this.logger.debug(message, meta);
return;
}
this.logger?.info?.(message, meta);
}
getResponseUrl(response) {
const request = response.request;
return request?.res?.responseUrl ?? '';