ERPNext (collab.vycept.com) now picks up every shop invoice with its PDF, books it as a Wabenhain sales invoice and matches it to the Stripe payment via the PaymentIntent, so fees, revenue and the invoice document sit on one booking. - GET /api/erp/rechnungen?seit=<ISO>&limit=<1-200>: invoices oldest first, with order, payment intent, customer address (no phone, no notes), items and the VAT split per rate exactly as printed (lib/vat.ts), including the shipping and discount share per rate, so ERPNext books the same cents. - GET /api/erp/rechnungen/<RE-YYYY-NNNN>/pdf: the stored invoice PDF. - Bearer token ERP_EXPORT_TOKEN (min. 32 chars, timing-safe compare); without it the export is off. Failed attempts are rate limited. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
280 lines
15 KiB
Bash
280 lines
15 KiB
Bash
# Datenbank: "postgresql" (Standard) oder "mysql". Nach einer Aenderung
|
||
# einmal `pnpm db:push` ausfuehren (schreibt prisma/schema.prisma neu und
|
||
# generiert den passenden Treiber-Adapter-Client) — siehe README, Abschnitt
|
||
# "Datenbank-Provider wechseln".
|
||
DATABASE_PROVIDER=postgresql
|
||
|
||
# PostgreSQL (Standard). Fuer docker compose siehe POSTGRES_* unten.
|
||
DATABASE_URL="postgresql://wabenhain:wabenhain@localhost:5432/wabenhain?schema=public"
|
||
# MySQL/MariaDB stattdessen:
|
||
# DATABASE_URL="mysql://wabenhain:wabenhain@localhost:3306/wabenhain"
|
||
|
||
# Postgres-Zugangsdaten fuer docker-compose.yml (Datenbank-Container)
|
||
POSTGRES_USER=wabenhain
|
||
POSTGRES_PASSWORD=change-me
|
||
POSTGRES_DB=wabenhain
|
||
|
||
# Nur noetig, wenn DATABASE_PROVIDER=mysql: Zugangsdaten fuer den
|
||
# db-mysql-Container ("docker compose --profile mysql up -d db-mysql")
|
||
MYSQL_USER=wabenhain
|
||
MYSQL_PASSWORD=change-me
|
||
MYSQL_DATABASE=wabenhain
|
||
|
||
# Stripe (Test-Keys aus dem Stripe-Dashboard, https://dashboard.stripe.com/test/apikeys)
|
||
#
|
||
# Serverseitiger Schluessel fuer Checkout-Sessions, Erstattungen und Webhooks.
|
||
# Empfohlen: ein eingeschraenkter Schluessel (rk_...) statt des vollen sk_...
|
||
STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxxxxx
|
||
# Oeffentlicher Schluessel fuer das eingebettete Zahlungsformular auf /checkout.
|
||
# Bewusst OHNE NEXT_PUBLIC_: wird zur Laufzeit gelesen, nicht beim Build
|
||
# eingebrannt. Muss zum Modus des geheimen Schluessels passen (test/live).
|
||
STRIPE_PUBLISHABLE_KEY=pk_test_xxxxxxxxxxxxxxxxxxxxxxxx
|
||
# Webhook-Secret aus `stripe listen` bzw. dem Stripe-Dashboard-Webhook-Endpoint
|
||
STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxx
|
||
|
||
# Admin-Zugang: Der interne /admin-Bereich wird NICHT per Basic-Auth
|
||
# geschuetzt, sondern ueber das normale Kundenkonto-Login (/konto) und die
|
||
# Rolle "ADMIN" am Konto (siehe README, Abschnitt "Admin-Zugang"). Diese
|
||
# beiden Werte legt `prisma db seed` als initiales Admin-Konto an, falls es
|
||
# noch nicht existiert. Eine UNVORHERSEHBARE E-Mail waehlen (nicht
|
||
# "admin@..." o.ae.) — sonst koennte sich vor dem ersten Seed-Lauf jemand
|
||
# genau diese Adresse selbst registrieren; prisma/seed.ts befoerdert ein
|
||
# bereits bestehendes Nicht-Admin-Konto zwar nicht mehr automatisch, ein
|
||
# unvorhersehbarer Wert ist trotzdem die robustere erste Verteidigungslinie.
|
||
ADMIN_EMAIL=[email protected]
|
||
ADMIN_PASSWORD=change-me
|
||
|
||
# Geheimer Schluessel zum Signieren der Kundenkonto-JWTs (HS256).
|
||
# In Produktion MINDESTENS 32 Zeichen (wird beim Start erzwungen).
|
||
# Erzeugen z.B. mit: openssl rand -base64 32
|
||
SESSION_SECRET=change-me-to-a-long-random-string
|
||
|
||
# Basis-URL der Seite (fuer absolute Links, z.B. in E-Mails/Sitemaps).
|
||
# Lokale Entwicklung (pnpm dev): http://localhost:3000 (Standard hier).
|
||
# Vollstaendiger Docker-Betrieb: http://localhost (Caddy laeuft auf Port 80,
|
||
# siehe SITE_ADDRESS/Caddyfile unten) bzw. die echte Domain in Produktion.
|
||
NEXT_PUBLIC_SITE_URL=http://localhost:3000
|
||
|
||
# Nur fuer den vollstaendigen Docker-Betrieb (docker-compose.yml, Caddyfile):
|
||
# die oeffentliche Domain, unter der die Seite erreichbar sein soll. Leer
|
||
# gelassen laeuft Caddy lokal per HTTP auf Port 80 ohne TLS — fuer eine
|
||
# echte Domain mit automatischem HTTPS z.B. "wabenhain.com" eintragen.
|
||
SITE_ADDRESS=
|
||
|
||
# Google Rezensionen (optional). Ohne diese beiden Werte zeigt die Startseite
|
||
# automatisch die statischen Beispiel-Stimmen als Fallback.
|
||
# API-Key: Google-Cloud-Projekt mit aktivierter "Places API (New)" — NICHT
|
||
# die gleichnamige alte API, die in neuen Projekten nicht mehr aktivierbar
|
||
# ist. Ausserdem noetig: Abrechnung am Projekt aktiv, und der Wert muss ein
|
||
# API-Schluessel sein (ein OAuth-Client funktioniert nicht). Keine
|
||
# Referrer-Beschraenkung setzen — der Abruf erfolgt serverseitig.
|
||
# Place-ID: https://developers.google.com/maps/documentation/places/web-service/place-id
|
||
GOOGLE_PLACES_API_KEY=
|
||
GOOGLE_PLACE_ID=
|
||
|
||
# Cloudflare Turnstile (optional). Ohne diese beiden Werte wird auf
|
||
# Registrierung, Newsletter und Firmenanfrage kein CAPTCHA angezeigt und die
|
||
# serverseitige Pruefung uebersprungen. Site-Key ist oeffentlich (NEXT_PUBLIC_),
|
||
# Secret-Key bleibt privat. Einrichtung: https://dash.cloudflare.com/?to=/:account/turnstile
|
||
TURNSTILE_SECRET_KEY=
|
||
NEXT_PUBLIC_TURNSTILE_SITE_KEY=
|
||
|
||
# Transaktions-E-Mails (Bestellbestaetigung, Newsletter-Opt-in,
|
||
# Passwort-Reset) gehen ueber Microsoft 365 mit OAuth 2.0 — mit derselben
|
||
# App-Registrierung wie "Mit Microsoft anmelden" (ENTRA_* weiter unten, dort
|
||
# zusaetzlich die Berechtigung SMTP.SendAsApp, siehe README
|
||
# "E-Mail-Versand"). Ein Passwort gibt es nicht. Ohne SMTP_USER und die
|
||
# ENTRA_*-Werte werden E-Mails nur geloggt statt versendet — praktisch fuer
|
||
# lokale Entwicklung ohne Mail-Setup.
|
||
# SMTP_USER = das Postfach, mit dem gesendet wird; SMTP_FROM sollte dieselbe
|
||
# Adresse tragen.
|
||
SMTP_USER=
|
||
SMTP_FROM="Wabenhain <[email protected]>"
|
||
|
||
# ApiCore-Synchronisierung (optional). Der Sync-Knopf im Admin-Dashboard holt
|
||
# Ernten, Chargen und Produkte aus ApiCores Shop-Schnittstelle (GET
|
||
# /shop-sync). Zugang ausschliesslich per API-Schluessel: in ApiCore unter
|
||
# Verwaltung -> Automatisierung -> API-Schluessel anlegen (Berechtigung
|
||
# "Shop-Daten lesen"). Kein Benutzerkonto, kein Passwort, jederzeit
|
||
# widerrufbar. Ohne beide Werte meldet der Knopf einen Konfigurationsfehler.
|
||
# Details siehe src/lib/apicore-client.ts.
|
||
APICORE_API_URL=
|
||
APICORE_API_KEY=
|
||
# Bestandsabgleich: der Schluessel braucht zusaetzlich "Verkaeufe und
|
||
# Rueckbuchungen des Shops buchen" (shop-sync:write). In ApiCore einen Webhook
|
||
# fuer "stock.changed" auf https://<shop>/api/apicore/webhook anlegen und das
|
||
# dort angezeigte Secret hier eintragen. Ohne Secret wird der Webhook
|
||
# abgelehnt; der CronJob gleicht dann alle 10 Minuten ab.
|
||
APICORE_WEBHOOK_SECRET=
|
||
|
||
# Rechnungsexport fuer die Buchhaltung (ERPNext, collab.vycept.com): ERPNext
|
||
# holt damit stuendlich alle Rechnungen samt PDF ab und ordnet sie der
|
||
# Stripe-Zahlung zu (GET /api/erp/rechnungen, nur lesend, siehe
|
||
# src/lib/erp-export.ts). Mindestens 32 Zeichen, z. B. `openssl rand -hex 32`;
|
||
# derselbe Wert steht in ERPNext als WABENHAIN_ERP_TOKEN. Leer = Export aus.
|
||
ERP_EXPORT_TOKEN=
|
||
|
||
# DHL Parcel DE Shipping API ("Post & Parcel Germany", developer.dhl.com) —
|
||
# ermoeglicht das Erstellen von DHL-Versandmarken per Knopfdruck im
|
||
# Admin-Bestelldetail (siehe src/lib/dhl.ts). Ohne diese fuenf Werte zeigt
|
||
# der Button dort nur den Hinweis "DHL ist noch nicht konfiguriert".
|
||
#
|
||
# So kommt ihr an die Werte:
|
||
# 1. Account anlegen auf https://developer.dhl.com und eine App registrieren,
|
||
# die die API "Parcel DE Shipping" (Post & Parcel Germany) einbindet.
|
||
# Die App liefert DHL_API_KEY (Consumer Key) und DHL_API_SECRET
|
||
# (Consumer Secret). Fuer den Sandbox-Test reichen diese beiden Werte
|
||
# plus die DHL-Testzugangsdaten unten.
|
||
# 2. Fuer den PRODUKTIV-Betrieb zusaetzlich einen DHL-Geschaeftskundenvertrag
|
||
# ("DHL Paket" Versandvertrag) abschliessen — Ansprechpartner ist der
|
||
# DHL-Vertrieb bzw. https://www.dhl.de/de/geschaeftskunden/paket.html.
|
||
# Darueber bekommt ihr:
|
||
# - Zugangsdaten zum Geschaeftskundenportal (GKP) -> DHL_GKP_USERNAME/
|
||
# DHL_GKP_PASSWORD (dieselben Zugangsdaten, mit denen ihr euch auf
|
||
# dhl.de/geschaeftskundenportal einloggt).
|
||
# - Die EKP (10-stellige Kundennummer) plus Verfahren/Teilnahme fuer
|
||
# das gebuchte Produkt (Standard-Inlandspaket V01PAK) -> zusammen
|
||
# ergibt das die 14-stellige DHL_ACCOUNT_NUMBER (billingNumber).
|
||
# 3. DHL_ENVIRONMENT auf "sandbox" lassen, bis alles funktioniert, dann auf
|
||
# "production" umstellen (unterschiedliche Basis-URLs, siehe lib/dhl.ts).
|
||
#
|
||
# DHL-Sandbox-Testzugangsdaten (Stand der Recherche, koennen sich aendern —
|
||
# im Zweifel im DHL-Developer-Portal unter der App nachschauen):
|
||
# DHL_GKP_USERNAME=user-valid
|
||
# DHL_GKP_PASSWORD=SandboxPasswort2023!
|
||
# HINWEIS: Fuer die PACKSTATION-SUCHE im Checkout reicht DHL_API_KEY allein.
|
||
# Sie laeuft ueber die DHL Location Finder API (lib/dhl-locations.ts), einen
|
||
# eigenen Dienst mit eigener Authentifizierung — nur der API-Key im Header,
|
||
# kein OAuth und kein Geschaeftskundenvertrag. Ist der Key gesetzt, bietet der
|
||
# Checkout "An eine Packstation" als Lieferziel an; fehlt er, erscheint die
|
||
# Option gar nicht erst. Die uebrigen DHL_*-Werte braucht erst das Erstellen
|
||
# von Versandmarken im Admin-Bereich. Kontingent laut DHL: 500 Aufrufe/Tag —
|
||
# der Endpunkt /api/shop/packstationen ist deshalb streng rate-limitiert und
|
||
# cached Ergebnisse 15 Minuten.
|
||
# SENDUNGSVERFOLGUNG auf /bestellung-verfolgen: laeuft ueber die DHL Shipment
|
||
# Tracking – Unified API (lib/dhl-tracking.ts), ebenfalls nur mit API-Key.
|
||
# Die DHL-App muss dafuer im Developer Portal zusaetzlich das Produkt
|
||
# "Shipment Tracking - Unified" abonniert haben. Ist es in einer EIGENEN App
|
||
# freigeschaltet, deren Key in DHL_TRACKING_API_KEY eintragen — sonst wird
|
||
# DHL_API_KEY verwendet. Ohne freigeschalteten Key zeigt die Seite wie bisher
|
||
# nur Sendungsnummer + Link zu dhl.de. Ergebnisse werden pro Sendung
|
||
# 10 Minuten (zugestellt: 12 Stunden) gecached, um das Kontingent zu schonen.
|
||
DHL_TRACKING_API_KEY=
|
||
DHL_API_KEY=
|
||
DHL_API_SECRET=
|
||
DHL_GKP_USERNAME=
|
||
DHL_GKP_PASSWORD=
|
||
# 14-stellig: EKP (10-stellig) + Verfahren (2-stellig) + Teilnahme (2-stellig)
|
||
DHL_ACCOUNT_NUMBER=
|
||
DHL_ENVIRONMENT=sandbox
|
||
|
||
# Microsoft Entra ID SSO ("Mit Microsoft anmelden", siehe
|
||
# src/app/api/auth/entra/). Ohne diese vier Werte zeigt der Button auf
|
||
# /konto einen Hinweis, dass SSO noch nicht konfiguriert ist, statt
|
||
# abzustuerzen.
|
||
#
|
||
# So richtet ihr es ein (Owner-Aufgabe im Azure-Portal, https://entra.microsoft.com):
|
||
# 1. Entra ID -> App registrations -> "New registration".
|
||
# - Name frei waehlbar, z.B. "Wabenhain Kundenlogin".
|
||
# - "Supported account types": i.d.R. "Accounts in this organizational
|
||
# directory only" (Single Tenant) fuer eine reine Firmen-SSO-Anbindung.
|
||
# - Redirect URI: Plattform "Web", URL = <eure Domain>/api/auth/entra/callback
|
||
# (lokal: http://localhost:3000/api/auth/entra/callback).
|
||
# 2. Nach dem Erstellen: "Application (client) ID" -> ENTRA_CLIENT_ID,
|
||
# "Directory (tenant) ID" -> ENTRA_TENANT_ID (beide auf der Overview-Seite
|
||
# der App-Registrierung sichtbar).
|
||
# 3. "Certificates & secrets" -> "New client secret" erzeugen -> der
|
||
# angezeigte WERT (nicht die Secret-ID!) -> ENTRA_CLIENT_SECRET. Nur
|
||
# einmal sichtbar, bei Verlust ein neues Secret erzeugen.
|
||
# 4. ENTRA_REDIRECT_URI = exakt dieselbe URL wie die in Schritt 1 hinterlegte
|
||
# Redirect-URI (muss zeichengenau uebereinstimmen).
|
||
# 5. "Enterprise applications" in Entra ID zeigt die App danach automatisch
|
||
# als Unternehmensapplikation (wird beim App-Registration-Schritt
|
||
# automatisch mit angelegt) — dort laesst sich optional der Nutzerkreis
|
||
# (z.B. nur bestimmte Gruppen) einschraenken.
|
||
# 6. Dieselbe App versendet auch die Shop-E-Mails (SMTP_USER oben): dafuer
|
||
# die Anwendungsberechtigung "Office 365 Exchange Online -> SMTP.SendAsApp"
|
||
# mit Administratorzustimmung ergaenzen und in Exchange das Postfach
|
||
# freigeben (README "E-Mail-Versand"). Laeuft das Secret ab, sind
|
||
# Anmeldung UND Mailversand betroffen.
|
||
ENTRA_TENANT_ID=
|
||
ENTRA_CLIENT_ID=
|
||
ENTRA_CLIENT_SECRET=
|
||
ENTRA_REDIRECT_URI=http://localhost:3000/api/auth/entra/callback
|
||
|
||
# Verzeichnis fuer hochgeladene Bilder (Inhalte-Bild in admin/inhalte,
|
||
# Produkt-/Erntenfotos in admin/produkte bzw. admin/ernten — siehe
|
||
# app/api/admin/upload/route.ts und app/uploads/[...path]/route.ts). Die App
|
||
# liest/schreibt hier ganz normal per node:fs; sie weiss nichts davon, WAS
|
||
# dieses Verzeichnis in Produktion ist (siehe unten).
|
||
#
|
||
# Lokal reicht der Default "./uploads" (relativ zum Projektverzeichnis, per
|
||
# .gitignore ausgeschlossen — beim ersten Upload automatisch angelegt).
|
||
#
|
||
# In docker-compose.yml ist UPLOAD_DIR bereits fest auf /app/uploads gesetzt
|
||
# und ueber ein benanntes Docker-Volume persistiert (siehe dort) — hier
|
||
# nichts weiter noetig, dieser Wert wirkt nur fuer lokales "npm run dev".
|
||
#
|
||
# In Produktion per Kubernetes (siehe k8s/05-app-deployment.yaml) zeigt
|
||
# UPLOAD_DIR stattdessen auf einen Volume-Mount, damit hochgeladene Bilder
|
||
# einen Pod-Neustart/-Redeploy ueberleben — welche Art Volume das konkret
|
||
# ist (SMB/NFS/hostPath/...), entscheidet die Betreiberin beim Anlegen des
|
||
# PersistentVolume; UPLOAD_DIR muss nur exakt auf den gewaehlten mountPath
|
||
# zeigen.
|
||
UPLOAD_DIR=./uploads
|
||
|
||
# Ablage der Rechnungs-PDFs, nach Jahr und Kunde sortiert:
|
||
# invoices/2026/mustermann-erika/RE-2026-A3F9C1B2.pdf
|
||
# Lokal reicht der Default "./invoices" (per .gitignore ausgeschlossen).
|
||
#
|
||
# MUSS ein anderes Verzeichnis sein als UPLOAD_DIR: auf die Uploads zeigt
|
||
# eine oeffentliche Route (app/uploads/[...path]), die JEDE Datei darunter
|
||
# ohne Anmeldung ausliefert. Laegen Rechnungen dort, koennte jede Person, die
|
||
# den Pfad erraet, fremde Rechnungen samt Anschrift herunterladen.
|
||
#
|
||
# In Produktion ein eigenes, gesichertes Volume: Rechnungen unterliegen in
|
||
# Deutschland zehn Jahren Aufbewahrungspflicht (§ 147 AO), und das PDF liegt
|
||
# NUR hier — die Datenbank kennt lediglich Nummer, Betrag und Pfad. Siehe
|
||
# k8s/04-uploads-pvc.yaml und den Sicherungs-CronJob k8s/07-backup-cronjob.yaml.
|
||
INVOICE_DIR=./invoices
|
||
|
||
# Umsatzsteuersatz in GANZEN Prozent, ausgewiesen auf jeder Rechnung.
|
||
# 7 = ermaessigter Satz (Honig als Lebensmittel, Anlage 2 zum UStG)
|
||
# 19 = voller Satz
|
||
# 0 = Kleinunternehmerregelung; die Rechnung traegt dann statt der
|
||
# Steuerzeilen den Pflichthinweis nach § 19 UStG
|
||
#
|
||
# Die Shop-Preise sind BRUTTOPREISE (gegenueber Verbrauchern ist der
|
||
# Gesamtpreis inkl. Umsatzsteuer anzugeben, § 1 PAngV). Die Steuer wird daraus
|
||
# HERAUSGERECHNET, nicht aufgeschlagen — der Endpreis bleibt genau der, den die
|
||
# Kundschaft im Warenkorb gesehen und bezahlt hat.
|
||
#
|
||
# Der Satz wird an jeder Rechnung festgeschrieben: eine Aenderung hier wirkt
|
||
# nur auf kuenftige Rechnungen, bereits ausgestellte behalten ihren Satz.
|
||
VAT_RATE_PERCENT=7
|
||
|
||
# Satz je Kategorie — der Steuersatz haengt an der Art der Ware. Fehlt hier
|
||
# ein Wert, gilt VAT_RATE_PERCENT von oben. Ein am einzelnen Produkt
|
||
# gepflegter Satz (Admin -> Produkte) schlaegt beides.
|
||
#
|
||
# PROPOLIS bitte pruefen: als Lebensmittel waeren 7% richtig, als
|
||
# Nahrungsergaenzungs- oder Kosmetikprodukt 19%. Voreingestellt ist der volle
|
||
# Satz, weil er im Zweifel der sicherere ist.
|
||
VAT_RATE_PERCENT_HONIG=7
|
||
VAT_RATE_PERCENT_KERZEN=19
|
||
VAT_RATE_PERCENT_PROPOLIS=19
|
||
# Weitere Bienenprodukte (Kategorie SONSTIGES, z. B. Salben, Wachstuecher)
|
||
VAT_RATE_PERCENT_SONSTIGES=19
|
||
|
||
# Threads, die ffmpeg beim Umrechnen eines hochgeladenen Videos benutzen darf
|
||
# (Admin -> Inhalte -> Hero-Video). Leer lassen heisst 2, erlaubt sind 1 bis 8.
|
||
#
|
||
# NICHT hochsetzen, um den Upload zu beschleunigen, ohne gleichzeitig das
|
||
# Speicherlimit des Pods zu pruefen: x264 haelt pro Thread eigene Bildpuffer,
|
||
# bei 4K-Material rund 12 MB je Bild. Ohne Deckel richtet sich ffmpeg nach den
|
||
# Kernen des KUBERNETES-KNOTENS statt nach dem CPU-Anteil des Pods — auf einem
|
||
# grossen Knoten sprengt das sowohl den Speicher des Pods als auch die
|
||
# Liveness-Probe, und der Container wird mitten im Upload neu gestartet.
|
||
# Ausfuehrliche Begruendung in src/lib/video-transcode.ts.
|
||
VIDEO_TRANSCODE_THREADS=
|