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
+120 -4
View File
@@ -31,6 +31,12 @@ from app.amazon_processor import (
close_interactive_login as amazon_close_interactive,
is_interactive_login_active as amazon_login_active,
)
from app.amazon_api import (
get_oauth_authorize_url,
exchange_auth_code,
check_api_configured,
process_amazon_api,
)
logging.basicConfig(
level=getattr(logging, os.environ.get("LOG_LEVEL", "INFO").upper(), logging.INFO),
@@ -388,6 +394,10 @@ async def api_amazon_settings(request: Request):
"amazon_email": body.get("amazon_email", ""),
"amazon_password": body.get("amazon_password") or current.get("amazon_password", ""),
"amazon_since_date": body.get("amazon_since_date", ""),
"amazon_mode": body.get("amazon_mode", "browser"),
"amazon_app_id": body.get("amazon_app_id", ""),
"amazon_client_id": body.get("amazon_client_id", ""),
"amazon_client_secret": body.get("amazon_client_secret") or current.get("amazon_client_secret", ""),
}
await save_settings(data)
return JSONResponse({"success": True})
@@ -395,9 +405,110 @@ async def api_amazon_settings(request: Request):
@app.get("/api/amazon-status")
async def api_amazon_status():
valid = await amazon_check_session()
login_active = amazon_login_active()
return JSONResponse({"session_valid": valid, "login_active": login_active})
settings = await get_settings()
mode = settings.get("amazon_mode", "browser")
if mode == "api":
api_status = await check_api_configured()
return JSONResponse({
"mode": "api",
"session_valid": api_status.get("authorized", False),
"login_active": False,
"api_configured": api_status.get("configured", False),
"api_authorized": api_status.get("authorized", False),
})
else:
valid = await amazon_check_session()
login_active = amazon_login_active()
return JSONResponse({
"mode": "browser",
"session_valid": valid,
"login_active": login_active,
})
def _get_oauth_redirect_uri(request: Request) -> str:
"""Get OAuth redirect URI from env var or request."""
base = os.environ.get("OAUTH_REDIRECT_BASE", "").rstrip("/")
if not base:
base = str(request.base_url).rstrip("/")
return f"{base}/api/amazon-oauth-callback"
@app.get("/api/amazon-oauth-url")
async def api_amazon_oauth_url(request: Request):
"""Generate OAuth authorization URL for Amazon Business API."""
settings = await get_settings()
app_id = settings.get("amazon_app_id", "")
if not app_id:
return JSONResponse({"error": "App-ID nicht konfiguriert"}, status_code=400)
redirect_uri = _get_oauth_redirect_uri(request)
domain = settings.get("amazon_domain", "amazon.de")
state = str(uuid.uuid4())
url = get_oauth_authorize_url(app_id, redirect_uri, domain, state)
return JSONResponse({"url": url, "state": state})
@app.get("/api/amazon-oauth-callback")
async def api_amazon_oauth_callback(request: Request):
"""Handle OAuth callback from Amazon."""
code = request.query_params.get("spapi_oauth_code") or request.query_params.get("code", "")
error = request.query_params.get("error", "")
if error:
return HTMLResponse(f"<h2>Autorisierung fehlgeschlagen</h2><p>{error}</p><p>Fenster kann geschlossen werden.</p>")
if not code:
return HTMLResponse("<h2>Fehler: Kein Autorisierungscode erhalten</h2><p>Fenster kann geschlossen werden.</p>")
settings = await get_settings()
client_id = settings.get("amazon_client_id", "")
client_secret = settings.get("amazon_client_secret", "")
redirect_uri = _get_oauth_redirect_uri(request)
result = await exchange_auth_code(code, client_id, client_secret, redirect_uri)
if "error" in result:
return HTMLResponse(f"<h2>Token-Exchange fehlgeschlagen</h2><p>{result['error']}</p>")
refresh_token = result.get("refresh_token", "")
if refresh_token:
await save_settings({"amazon_refresh_token": refresh_token})
return HTMLResponse(
"<h2>Autorisierung erfolgreich!</h2>"
"<p>Refresh-Token wurde gespeichert. Dieses Fenster kann geschlossen werden.</p>"
"<script>window.close();</script>"
)
return HTMLResponse("<h2>Fehler: Kein Refresh-Token erhalten</h2>")
@app.post("/api/amazon-oauth-exchange")
async def api_amazon_oauth_exchange(request: Request):
"""Manual OAuth code exchange - user pastes the code from the redirect URL."""
body = await request.json()
code = body.get("code", "").strip()
if not code:
return JSONResponse({"error": "Kein Code angegeben"}, status_code=400)
settings = await get_settings()
client_id = settings.get("amazon_client_id", "")
client_secret = settings.get("amazon_client_secret", "")
redirect_uri = _get_oauth_redirect_uri(request)
result = await exchange_auth_code(code, client_id, client_secret, redirect_uri)
if "error" in result:
return JSONResponse({"error": result["error"]}, status_code=400)
refresh_token = result.get("refresh_token", "")
if refresh_token:
await save_settings({"amazon_refresh_token": refresh_token})
return JSONResponse({"success": True})
return JSONResponse({"error": "Kein Refresh-Token erhalten"}, status_code=400)
@app.post("/api/amazon-login")
@@ -462,7 +573,12 @@ async def api_amazon_logout():
@app.post("/api/amazon-process")
async def api_amazon_process():
result = await process_amazon()
settings = await get_settings()
mode = settings.get("amazon_mode", "browser")
if mode == "api":
result = await process_amazon_api()
else:
result = await process_amazon()
return JSONResponse(result)