v2.2: Verkaufsfertig-Fundament — Mollie/Payment-Abstraktion, MwSt/Grundpreis (PAngV), Versandzonen, Bestellmails (Listmonk/SMTP/Log), Feature-Flags
- payments.js: einheitliche createPayment/Webhook-Schnittstelle (Mollie Default, Stripe, Demo); Auto-Provider-Wahl; Mollie-REST + /api/payments/webhook (idempotent); Fake-Key => sauberer Demo-Fallback - mailer.js: sendMail via Listmonk-Tx / SMTP (nodemailer) / Log-Fallback (email_log); gebrandete Bestellbestaetigung bei paid - DACH: products.mwst + base_amount/base_unit/base_price_per (Grundpreis); Storefront/Warenkorb/Checkout/Erfolg/Admin mit MwSt-Ausweis + Versand-Transparenz; tax_cents/shipping_cents/country an Orders - shipping_zones-Tabelle + CRUD + shippingFor(); Admin 'Versand'; serverseitige Versandberechnung in /api/checkout + /api/shipping-quote (Laenderwahl live) - Feature-Flags (feature_*) + feature()-Helper; Admin Module-Toggles; Newsletter-Gating (Popup/Subscribe) - Admin-API/Manifest/ai-admin.txt um shipping_zones erweitert; MCP list/upsert/delete_shipping; README/.env.example ergaenzt; Version 2.2.0
This commit is contained in:
@@ -2,27 +2,51 @@
|
||||
import Admin from '../../../layouts/Admin.astro';
|
||||
import { adminBase } from '../../../lib/auth.js';
|
||||
const base = adminBase();
|
||||
import { getSettings, setSetting } from '../../../lib/store.js';
|
||||
import { getSettings, setSetting, resolvePaymentProvider, FEATURE_KEYS, feature } from '../../../lib/store.js';
|
||||
import { mailerStatus } from '../../../lib/mailer.js';
|
||||
|
||||
const FEATURE_LABELS = {
|
||||
feature_newsletter: ['Newsletter', 'Newsletter-Popup & Anmeldung im Storefront'],
|
||||
feature_accounts: ['Kundenkonten', 'Registrierung & Login für Kund:innen (in Vorbereitung)'],
|
||||
feature_reviews: ['Bewertungen', 'Produktbewertungen (in Vorbereitung)'],
|
||||
feature_wishlist: ['Merkliste', 'Wunschliste / Merken (in Vorbereitung)'],
|
||||
feature_abandoned_cart: ['Warenkorb-Erinnerung', 'Abandoned-Cart-Mails (in Vorbereitung)'],
|
||||
feature_search: ['Suche', 'Produktsuche im Storefront (in Vorbereitung)'],
|
||||
};
|
||||
|
||||
let flash = '';
|
||||
if (Astro.request.method === 'POST') {
|
||||
const f = await Astro.request.formData();
|
||||
setSetting('shop_name', f.get('shop_name') || 'hd-commerce');
|
||||
setSetting('shop_tagline', f.get('shop_tagline') || '');
|
||||
setSetting('shop_email', f.get('shop_email') || '');
|
||||
setSetting('brand_accent', f.get('brand_accent') || '#b8566a');
|
||||
setSetting('brand_accent_dark', f.get('brand_accent_dark') || '#8d3f50');
|
||||
setSetting('currency', f.get('currency') || 'EUR');
|
||||
setSetting('free_shipping_cents', String(Math.round(parseFloat(String(f.get('free_shipping') || '49').replace(',', '.')) * 100) || 4900));
|
||||
flash = 'Einstellungen gespeichert.';
|
||||
const action = String(f.get('_action') || 'general');
|
||||
if (action === 'features') {
|
||||
for (const k of FEATURE_KEYS) setSetting(k, f.get(k) === 'on' ? '1' : '0');
|
||||
flash = 'Module aktualisiert.';
|
||||
} else if (action === 'payment') {
|
||||
setSetting('payment_provider', String(f.get('payment_provider') || ''));
|
||||
flash = 'Zahlungsanbieter gespeichert.';
|
||||
} else {
|
||||
setSetting('shop_name', f.get('shop_name') || 'hd-commerce');
|
||||
setSetting('shop_tagline', f.get('shop_tagline') || '');
|
||||
setSetting('shop_email', f.get('shop_email') || '');
|
||||
setSetting('brand_accent', f.get('brand_accent') || '#b8566a');
|
||||
setSetting('brand_accent_dark', f.get('brand_accent_dark') || '#8d3f50');
|
||||
setSetting('currency', f.get('currency') || 'EUR');
|
||||
setSetting('free_shipping_cents', String(Math.round(parseFloat(String(f.get('free_shipping') || '49').replace(',', '.')) * 100) || 4900));
|
||||
flash = 'Einstellungen gespeichert.';
|
||||
}
|
||||
}
|
||||
|
||||
const s = getSettings();
|
||||
const stripeSecret = (process.env.STRIPE_SECRET_KEY || '').trim();
|
||||
const stripeReal = /^sk_(test|live)_[A-Za-z0-9]{16,}/.test(stripeSecret);
|
||||
const stripeMode = stripeReal ? (stripeSecret.startsWith('sk_live') ? 'Live' : 'Test') : 'Demo-Fallback';
|
||||
const freeShipStr = ((Number(s.free_shipping_cents) || 4900) / 100).toFixed(2).replace('.', ',');
|
||||
const currencies = ['EUR', 'CHF', 'USD', 'GBP'];
|
||||
|
||||
const pp = resolvePaymentProvider();
|
||||
const providerLabel = { mollie: 'Mollie', stripe: 'Stripe', demo: 'Demo-Fallback' }[pp.provider] || pp.provider;
|
||||
const mollieSet = /^(test|live)_\w{20,}/.test((process.env.MOLLIE_API_KEY || '').trim());
|
||||
const stripeSet = /^sk_(test|live)_[A-Za-z0-9]{16,}/.test((process.env.STRIPE_SECRET_KEY || '').trim());
|
||||
const providerSetting = s.payment_provider || '';
|
||||
|
||||
const mail = mailerStatus();
|
||||
---
|
||||
<Admin title="Einstellungen" active="einstellungen" crumbs={[{ label: 'Einstellungen' }]}>
|
||||
<div class="s-stack">
|
||||
@@ -51,26 +75,62 @@ const currencies = ['EUR', 'CHF', 'USD', 'GBP'];
|
||||
<div class="s-field"><label class="s-label">Währung</label><select class="s-select" name="currency">{currencies.map((c) => (<option value={c} selected={s.currency === c}>{c}</option>))}</select></div>
|
||||
<div class="s-field"><label class="s-label">Gratis-Versand ab (€)</label><input class="s-input" name="free_shipping" value={freeShipStr} /></div>
|
||||
</div>
|
||||
<div class="s-help">Versandzonen & länderabhängige Preise unter „Versand".</div>
|
||||
</div>
|
||||
<button class="s-btn s-btn-primary" type="submit" style="align-self:flex-start">Alle Einstellungen speichern</button>
|
||||
<button class="s-btn s-btn-primary" type="submit" style="align-self:flex-start">Einstellungen speichern</button>
|
||||
</div>
|
||||
|
||||
<div class="s-stack">
|
||||
<div class="s-card s-card-pad">
|
||||
<div class="s-section-title" style="margin-bottom:12px">Zahlung (Stripe)</div>
|
||||
<p style="margin:0 0 8px"><span class={`s-badge ${stripeReal ? 'green' : 'amber'}`}>{stripeMode}</span></p>
|
||||
<p class="s-help">{stripeReal ? 'Echter Stripe-Schlüssel erkannt — Checkout nutzt Stripe Hosted Checkout.' : 'Kein echter STRIPE_SECRET_KEY gesetzt. Der Checkout läuft im Demo-Fallback (Bestellung ohne Zahlung).'}</p>
|
||||
<p class="s-help" style="margin-top:8px">Konfiguration über ENV: <b>STRIPE_SECRET_KEY</b>, <b>STRIPE_PUBLIC_KEY</b>.</p>
|
||||
<div class="s-section-title" style="margin-bottom:12px">Zahlung</div>
|
||||
<p style="margin:0 0 10px"><span class={`s-badge ${pp.provider === 'demo' ? 'amber' : 'green'}`}>Aktiv: {providerLabel}</span> <span class="s-badge gray" style="margin-left:6px">{pp.source === 'auto' ? 'Auto-Wahl' : pp.source === 'setting' ? 'manuell' : 'ENV'}</span></p>
|
||||
<div class="s-field"><label class="s-label">Anbieter</label>
|
||||
<select class="s-select" name="payment_provider" form="payForm">
|
||||
<option value="" selected={providerSetting === ''}>Automatisch (nach Keys)</option>
|
||||
<option value="mollie" selected={providerSetting === 'mollie'}>Mollie</option>
|
||||
<option value="stripe" selected={providerSetting === 'stripe'}>Stripe</option>
|
||||
<option value="demo" selected={providerSetting === 'demo'}>Demo (ohne Zahlung)</option>
|
||||
</select>
|
||||
</div>
|
||||
<ul class="s-help" style="margin:8px 0 0;padding-left:16px;line-height:1.7">
|
||||
<li>Mollie-Key (MOLLIE_API_KEY): <b>{mollieSet ? 'gesetzt' : 'fehlt'}</b></li>
|
||||
<li>Stripe-Key (STRIPE_SECRET_KEY): <b>{stripeSet ? 'gesetzt' : 'fehlt'}</b></li>
|
||||
</ul>
|
||||
{pp.provider === 'demo' && <p class="s-help" style="margin-top:8px">Ohne gültigen Key läuft der Checkout im Demo-Fallback (Bestellung ohne echte Zahlung).</p>}
|
||||
</div>
|
||||
|
||||
<div class="s-card s-card-pad">
|
||||
<div class="s-section-title" style="margin-bottom:12px">Analytics</div>
|
||||
<p class="s-help">hd-commerce nutzt eine eigene First-Party-Statistik (events-Tabelle). Kein externer Dienst, keine personenbezogenen Rohdaten — die Session-Kennung ist ein täglich rollender Hash.</p>
|
||||
<div class="s-section-title" style="margin-bottom:12px">E-Mail-Versand</div>
|
||||
<p style="margin:0 0 8px"><span class={`s-badge ${mail.provider === 'log' ? 'amber' : (mail.configured ? 'green' : 'red')}`}>{mail.provider === 'log' ? 'Log-Fallback' : mail.provider} {mail.provider !== 'log' ? (mail.configured ? '· konfiguriert' : '· unvollständig') : ''}</span></p>
|
||||
<p class="s-help">Absender: <b>{mail.from}</b></p>
|
||||
<p class="s-help" style="margin-top:6px">Bestellbestätigungen werden bei bezahlter Bestellung versendet. Ohne Provider-ENV landet die Mail im <a href={base + '/einstellungen/email-log'}>E-Mail-Log</a>.</p>
|
||||
<p class="s-help" style="margin-top:6px">ENV: <b>MAIL_PROVIDER</b> (listmonk|smtp), <b>MAIL_FROM</b>, Listmonk-/SMTP-Variablen.</p>
|
||||
</div>
|
||||
|
||||
<div class="s-card s-card-pad">
|
||||
<div class="s-section-title" style="margin-bottom:12px">System</div>
|
||||
<p class="s-help">Datenbank: SQLite (<b>DB_PATH</b>). Admin-Zugang über Session-Login; Initial-Owner aus <b>ADMIN_EMAIL</b> / <b>ADMIN_PASS</b>. Admin-Pfad über <b>ADMIN_PATH</b>. Nutzer & Rollen unter „Nutzer & Zugänge".</p>
|
||||
<p class="s-help">Datenbank: SQLite (<b>DB_PATH</b>). Admin-Zugang über Session-Login; Initial-Owner aus <b>ADMIN_EMAIL</b> / <b>ADMIN_PASS</b>. Admin-Pfad über <b>ADMIN_PATH</b>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form method="POST" id="payForm"><input type="hidden" name="_action" value="payment" /><button class="s-btn s-btn-primary" type="submit">Zahlungsanbieter speichern</button></form>
|
||||
|
||||
<div class="s-card s-card-pad">
|
||||
<div class="s-section-title" style="margin-bottom:6px;font-size:15px">Module (Feature-Flags)</div>
|
||||
<p class="s-help" style="margin:0 0 14px">Schalte einzelne Funktionen zentral an oder aus. Abgeschaltete Module verschwinden aus Storefront und Admin.</p>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="_action" value="features" />
|
||||
<div class="s-form-grid" style="grid-template-columns:1fr 1fr;gap:14px">
|
||||
{FEATURE_KEYS.map((k) => (
|
||||
<label class="s-card" style="display:flex;gap:12px;align-items:flex-start;padding:14px;cursor:pointer">
|
||||
<input type="checkbox" name={k} checked={feature(k)} style="margin-top:3px;width:18px;height:18px;accent-color:var(--accent)" />
|
||||
<span><b style="display:block">{FEATURE_LABELS[k][0]}</b><span class="s-help" style="margin:0">{FEATURE_LABELS[k][1]}</span></span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<button class="s-btn s-btn-primary" type="submit" style="margin-top:14px">Module speichern</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Admin>
|
||||
|
||||
Reference in New Issue
Block a user