Amazon Business API integration replacing browser automation

- Add amazon_api.py with Reconciliation + Document API client
- OAuth flow with manual code exchange for local installations
- Dual mode: API (recommended) or Browser automation (fallback)
- New settings: amazon_app_id, amazon_client_id, amazon_client_secret, amazon_refresh_token
- Platform UI with mode switcher, API credential fields, OAuth button
- Scheduler supports both API and browser modes
- README with full Amazon API setup guide
- httpx added for async HTTP requests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-05 18:08:35 +02:00
parent a4e39332c7
commit 337e0e99a5
9 changed files with 1130 additions and 50 deletions
+203 -36
View File
@@ -17,6 +17,13 @@
<option value="true" {% if settings.get('amazon_enabled') == 'true' %}selected{% endif %}>Aktiviert</option>
</select>
</div>
<div class="form-group">
<label for="amazon_mode">Abruf-Modus</label>
<select id="amazon_mode" name="amazon_mode" onchange="toggleAmazonMode()">
<option value="api" {% if settings.get('amazon_mode') == 'api' %}selected{% endif %}>API (empfohlen)</option>
<option value="browser" {% if settings.get('amazon_mode', 'browser') == 'browser' %}selected{% endif %}>Browser-Automation</option>
</select>
</div>
<div class="form-group">
<label for="amazon_domain">Amazon-Domain</label>
<select id="amazon_domain" name="amazon_domain">
@@ -25,29 +32,71 @@
<option value="amazon.com" {% if settings.get('amazon_domain') == 'amazon.com' %}selected{% endif %}>amazon.com</option>
</select>
</div>
<div class="form-group">
<label for="amazon_email">Amazon E-Mail</label>
<input type="email" id="amazon_email" name="amazon_email"
value="{{ settings.get('amazon_email', '') }}"
placeholder="email@example.com">
</div>
<div class="form-group">
<label for="amazon_password">Amazon Passwort</label>
<input type="password" id="amazon_password" name="amazon_password"
placeholder="{% if settings.get('amazon_password') %}(gespeichert){% else %}Passwort eingeben{% endif %}">
</div>
<div class="form-group">
<label for="amazon_since_date">Rechnungen ab Datum</label>
<input type="date" id="amazon_since_date" name="amazon_since_date"
value="{{ settings.get('amazon_since_date', '') }}">
<small class="text-muted">Leer = letzte 30 Tage</small>
</div>
<div class="form-group" style="align-self:end;">
{% if settings.get('amazon_last_sync') %}
<small class="text-muted">Letzter Abruf: {{ settings.get('amazon_last_sync') }}</small>
{% endif %}
</div>
<!-- API Mode Fields -->
<div id="apiFields" style="{% if settings.get('amazon_mode', 'browser') != 'api' %}display:none;{% endif %}margin-top:1rem;">
<h3 style="font-size:1rem;margin-bottom:0.75rem;">API-Zugangsdaten (Amazon Business API)</h3>
<div class="form-grid">
<div class="form-group">
<label for="amazon_app_id">App-ID</label>
<input type="text" id="amazon_app_id" name="amazon_app_id"
value="{{ settings.get('amazon_app_id', '') }}"
placeholder="amzn1.sp.solution.xxxxx">
<small class="text-muted">Aus dem Solution Provider Portal</small>
</div>
<div class="form-group">
<label for="amazon_client_id">Client-ID (LWA)</label>
<input type="text" id="amazon_client_id" name="amazon_client_id"
value="{{ settings.get('amazon_client_id', '') }}"
placeholder="amzn1.application-oa2-client.xxxxx">
</div>
<div class="form-group">
<label for="amazon_client_secret">Client-Sicherheitsschluessel (LWA)</label>
<input type="password" id="amazon_client_secret" name="amazon_client_secret"
placeholder="{% if settings.get('amazon_client_secret') %}(gespeichert){% else %}Client Secret eingeben{% endif %}">
</div>
<div class="form-group" style="align-self:end;">
<small class="text-muted">
{% if settings.get('amazon_refresh_token') %}
Refresh-Token: gespeichert
{% else %}
Refresh-Token: fehlt - bitte autorisieren
{% endif %}
</small>
</div>
</div>
</div>
<!-- Browser Mode Fields -->
<div id="browserFields" style="{% if settings.get('amazon_mode', 'browser') != 'browser' %}display:none;{% endif %}margin-top:1rem;">
<h3 style="font-size:1rem;margin-bottom:0.75rem;">Browser-Zugangsdaten</h3>
<div class="form-grid">
<div class="form-group">
<label for="amazon_email">Amazon E-Mail</label>
<input type="email" id="amazon_email" name="amazon_email"
value="{{ settings.get('amazon_email', '') }}"
placeholder="email@example.com">
</div>
<div class="form-group">
<label for="amazon_password">Amazon Passwort</label>
<input type="password" id="amazon_password" name="amazon_password"
placeholder="{% if settings.get('amazon_password') %}(gespeichert){% else %}Passwort eingeben{% endif %}">
</div>
</div>
</div>
<div style="margin-top:1rem;">
{% if settings.get('amazon_last_sync') %}
<small class="text-muted">Letzter Abruf: {{ settings.get('amazon_last_sync') }}</small>
{% endif %}
</div>
<div class="form-actions" style="margin-top:1rem;">
<button type="button" class="btn btn-primary" onclick="saveAmazonSettings()">Einstellungen speichern</button>
</div>
@@ -57,16 +106,41 @@
<div class="card">
<h2>Anmeldung &amp; Abruf</h2>
<div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:1rem;">
<span>Session:</span>
<span>Status:</span>
<span id="sessionBadge" class="badge badge-inactive">Wird geprüft...</span>
</div>
<div style="display:flex;gap:0.75rem;flex-wrap:wrap;">
<button type="button" id="btnLogin" class="btn btn-primary" onclick="doLogin()">Bei Amazon anmelden</button>
<button type="button" id="btnLogout" class="btn btn-secondary" onclick="doLogout()" style="display:none;">Session löschen</button>
<button type="button" id="btnProcess" class="btn btn-success" onclick="doProcess()" style="display:none;">Jetzt Rechnungen abrufen</button>
<button type="button" class="btn btn-secondary" onclick="doReset()">Importierte zurücksetzen</button>
<!-- API Mode Buttons -->
<div id="apiButtons" style="display:none;">
<div style="display:flex;gap:0.75rem;flex-wrap:wrap;align-items:center;">
<button type="button" id="btnOAuth" class="btn btn-primary" onclick="doOAuth()">Bei Amazon autorisieren</button>
<button type="button" id="btnProcessApi" class="btn btn-success" onclick="doProcess()" style="display:none;">Jetzt Rechnungen abrufen</button>
<button type="button" class="btn btn-secondary" onclick="doReset()">Importierte zurücksetzen</button>
</div>
<!-- Manual code exchange for local setups -->
<div id="oauthCodeBox" style="display:none;margin-top:1rem;">
<p class="text-muted" style="font-size:0.85rem;">
Nach der Autorisierung bei Amazon werden Sie zu einer Seite weitergeleitet die nicht laden wird.
Kopieren Sie den Wert von <code>spapi_oauth_code</code> aus der URL-Leiste und tragen ihn hier ein:
</p>
<div style="display:flex;gap:0.5rem;align-items:center;">
<input type="text" id="oauthCode" placeholder="spapi_oauth_code hier einfuegen..."
style="flex:1;padding:0.5rem;border:1px solid var(--border-color);border-radius:4px;background:var(--input-bg);color:var(--text-color);">
<button type="button" class="btn btn-primary" onclick="exchangeCode()">Token tauschen</button>
</div>
</div>
</div>
<!-- Browser Mode Buttons -->
<div id="browserButtons">
<div style="display:flex;gap:0.75rem;flex-wrap:wrap;">
<button type="button" id="btnLogin" class="btn btn-primary" onclick="doLogin()">Bei Amazon anmelden</button>
<button type="button" id="btnLogout" class="btn btn-secondary" onclick="doLogout()" style="display:none;">Session löschen</button>
<button type="button" id="btnProcess" class="btn btn-success" onclick="doProcess()" style="display:none;">Jetzt Rechnungen abrufen</button>
<button type="button" class="btn btn-secondary" onclick="doReset()">Importierte zurücksetzen</button>
</div>
</div>
<div id="processMsg" style="margin-top:0.75rem;"></div>
</div>
@@ -97,6 +171,16 @@
</div>
<script>
// --- Mode Toggle ---
function toggleAmazonMode() {
const mode = document.getElementById('amazon_mode').value;
document.getElementById('apiFields').style.display = mode === 'api' ? '' : 'none';
document.getElementById('browserFields').style.display = mode === 'browser' ? '' : 'none';
document.getElementById('apiButtons').style.display = mode === 'api' ? '' : 'none';
document.getElementById('browserButtons').style.display = mode === 'browser' ? '' : 'none';
checkSession();
}
// --- Settings ---
async function saveAmazonSettings() {
const btn = event.target;
@@ -106,10 +190,14 @@ async function saveAmazonSettings() {
const data = {
amazon_enabled: document.getElementById('amazon_enabled').value,
amazon_mode: document.getElementById('amazon_mode').value,
amazon_domain: document.getElementById('amazon_domain').value,
amazon_email: document.getElementById('amazon_email').value,
amazon_password: document.getElementById('amazon_password').value,
amazon_since_date: document.getElementById('amazon_since_date').value,
amazon_app_id: document.getElementById('amazon_app_id').value,
amazon_client_id: document.getElementById('amazon_client_id').value,
amazon_client_secret: document.getElementById('amazon_client_secret').value,
};
try {
@@ -142,21 +230,43 @@ async function checkSession() {
try {
const resp = await fetch('/api/amazon-status');
const data = await resp.json();
if (data.login_active) {
badge.className = 'badge badge-warning';
badge.textContent = 'Login läuft...';
document.getElementById('btnLogout').style.display = 'none';
document.getElementById('btnProcess').style.display = 'none';
} else if (data.session_valid) {
badge.className = 'badge badge-success';
badge.textContent = 'Angemeldet';
document.getElementById('btnLogout').style.display = '';
document.getElementById('btnProcess').style.display = '';
if (data.mode === 'api') {
// API mode
if (data.api_authorized) {
badge.className = 'badge badge-success';
badge.textContent = 'API autorisiert';
document.getElementById('btnOAuth').style.display = 'none';
document.getElementById('btnProcessApi').style.display = '';
} else if (data.api_configured) {
badge.className = 'badge badge-warning';
badge.textContent = 'Nicht autorisiert';
document.getElementById('btnOAuth').style.display = '';
document.getElementById('btnProcessApi').style.display = 'none';
} else {
badge.className = 'badge badge-inactive';
badge.textContent = 'API nicht konfiguriert';
document.getElementById('btnOAuth').style.display = 'none';
document.getElementById('btnProcessApi').style.display = 'none';
}
} else {
badge.className = 'badge badge-inactive';
badge.textContent = 'Nicht angemeldet';
document.getElementById('btnLogout').style.display = 'none';
document.getElementById('btnProcess').style.display = 'none';
// Browser mode
if (data.login_active) {
badge.className = 'badge badge-warning';
badge.textContent = 'Login läuft...';
document.getElementById('btnLogout').style.display = 'none';
document.getElementById('btnProcess').style.display = 'none';
} else if (data.session_valid) {
badge.className = 'badge badge-success';
badge.textContent = 'Angemeldet';
document.getElementById('btnLogout').style.display = '';
document.getElementById('btnProcess').style.display = '';
} else {
badge.className = 'badge badge-inactive';
badge.textContent = 'Nicht angemeldet';
document.getElementById('btnLogout').style.display = 'none';
document.getElementById('btnProcess').style.display = 'none';
}
}
} catch (e) {
badge.className = 'badge badge-inactive';
@@ -164,6 +274,62 @@ async function checkSession() {
}
}
// --- OAuth Authorization (API mode) ---
async function doOAuth() {
const msgEl = document.getElementById('processMsg');
try {
const resp = await fetch('/api/amazon-oauth-url');
const data = await resp.json();
if (data.url) {
window.open(data.url, '_blank');
// Show code input box for manual exchange
document.getElementById('oauthCodeBox').style.display = '';
msgEl.innerHTML = '<div class="alert alert-info">Amazon-Autorisierungsseite wurde geöffnet. Nach der Autorisierung den Code aus der URL kopieren und unten eintragen.</div>';
} else {
msgEl.innerHTML = '<div class="alert alert-error">' + escapeHtml(data.error || 'Fehler') + '</div>';
}
} catch (e) {
msgEl.innerHTML = '<div class="alert alert-error">Verbindungsfehler</div>';
}
}
async function exchangeCode() {
const codeInput = document.getElementById('oauthCode');
let code = codeInput.value.trim();
const msgEl = document.getElementById('processMsg');
if (!code) {
msgEl.innerHTML = '<div class="alert alert-error">Bitte den Code eintragen</div>';
return;
}
// If user pasted the full URL, extract the code
if (code.includes('spapi_oauth_code=')) {
const url = new URL(code.startsWith('http') ? code : 'https://x.com?' + code);
code = url.searchParams.get('spapi_oauth_code') || code;
}
msgEl.innerHTML = '<div class="alert alert-info">Token wird getauscht...</div>';
try {
const resp = await fetch('/api/amazon-oauth-exchange', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({code}),
});
const data = await resp.json();
if (data.success) {
msgEl.innerHTML = '<div class="alert alert-success">Erfolgreich autorisiert! Refresh-Token gespeichert.</div>';
document.getElementById('oauthCodeBox').style.display = 'none';
checkSession();
} else {
msgEl.innerHTML = '<div class="alert alert-error">' + escapeHtml(data.error || 'Token-Exchange fehlgeschlagen') + '</div>';
}
} catch (e) {
msgEl.innerHTML = '<div class="alert alert-error">Verbindungsfehler</div>';
}
}
// --- Interactive Browser Login ---
let screenshotInterval = null;
let loginPollInterval = null;
@@ -357,7 +523,8 @@ function escapeHtml(str) {
return d.innerHTML;
}
// Initial check
// Initial setup
toggleAmazonMode();
checkSession();
</script>
{% endblock %}