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:
2026-06-17 16:37:10 +00:00
parent 430fa718fa
commit e5514dd5da
31 changed files with 1077 additions and 129 deletions
+7 -1
View File
@@ -13,6 +13,7 @@ if (Astro.request.method === 'POST') {
}
const order = getOrderById(id);
if (!order) return Astro.redirect(base + '/bestellungen');
const subtotal = order.total_cents + (order.discount_cents || 0) - (order.shipping_cents || 0);
const statusMap = { fulfilled: ['green', 'Erfüllt'], pending: ['amber', 'Offen'], cancelled: ['gray', 'Storniert'], refunded: ['red', 'Erstattet'] };
const fmtDate = (s) => new Date(s).toLocaleString('de-DE', { day: '2-digit', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit' });
const statuses = [['pending', 'Offen'], ['fulfilled', 'Erfüllt'], ['cancelled', 'Storniert'], ['refunded', 'Erstattet']];
@@ -39,7 +40,12 @@ const statuses = [['pending', 'Offen'], ['fulfilled', 'Erfüllt'], ['cancelled',
{(order.discount_cents === 0 && order.discount_code) && (
<div class="s-card-pad" style="display:flex;justify-content:space-between;color:var(--accent);font-size:14px;border-top:1px solid var(--s-border)"><span>Gutschein ({order.discount_code})</span><span>Gratisversand</span></div>
)}
<div class="s-card-pad" style="display:flex;justify-content:space-between;font-weight:700;font-size:16px;border-top:1px solid var(--s-border)"><span>Gesamt</span><span>{formatPrice(order.total_cents)}</span></div>
<div class="s-card-pad" style="display:flex;justify-content:space-between;font-size:14px;color:var(--s-muted,#777);border-top:1px solid var(--s-border)"><span>Zwischensumme (netto + brutto)</span><span>{formatPrice(subtotal)}</span></div>
<div class="s-card-pad" style="display:flex;justify-content:space-between;font-size:14px;color:var(--s-muted,#777);padding-top:0"><span>Versand{order.country ? ` (${order.country})` : ''}</span><span>{order.shipping_cents === 0 ? 'Kostenlos' : formatPrice(order.shipping_cents)}</span></div>
<div class="s-card-pad" style="display:flex;justify-content:space-between;font-weight:700;font-size:16px;border-top:1px solid var(--s-border)"><span>Gesamt (brutto)</span><span>{formatPrice(order.total_cents)}</span></div>
<div class="s-card-pad" style="display:flex;justify-content:space-between;font-size:13px;color:var(--s-muted,#777);padding-top:0">
<span>Netto / enthaltene MwSt</span><span>{formatPrice(order.total_cents - (order.tax_cents||0))} / {formatPrice(order.tax_cents||0)}</span>
</div>
</div>
<div class="s-stack">