Files
hd-commerce/src/lib/admin-api.js
T
till e5514dd5da 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
2026-06-17 16:37:10 +00:00

144 lines
8.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// hd-commerce — Token-gesicherte Admin-JSON-API (für KI/MCP).
// Bearer-Token aus ENV HDC_API_TOKEN (getrennt von der Session-Auth).
import * as store from './store.js';
import { BLOCK_TYPES } from './blocks.js';
export function json(obj, status = 200) {
return new Response(JSON.stringify(obj, null, 2), { status, headers: { 'Content-Type': 'application/json; charset=utf-8' } });
}
export function authOk(request) {
const token = (process.env.HDC_API_TOKEN || '').trim();
if (!token) return false; // ohne konfiguriertes Token bleibt die API gesperrt
const hdr = request.headers.get('authorization') || '';
const m = hdr.match(/^Bearer\s+(.+)$/i);
return !!m && m[1].trim() === token;
}
// ---- Ressourcen-Definitionen für das Manifest ----
export const RESOURCES = {
products: { rw: true, fields: ['slug', 'name', 'shortName', 'priceCents', 'mwst(0|7|19)', 'base_amount', 'base_unit', 'base_price_per', 'category', 'sizes[]', 'images[]', 'cardImage', 'badge', 'stock', 'material', 'features[]', 'featured', 'sort', 'desc', 'metafields{}'] },
pages: { rw: true, fields: ['slug', 'title', 'body', 'type(content|legal)', 'active', 'sort', 'blocks[]'] },
slides: { rw: true, fields: ['image', 'headline', 'subline', 'link', 'sort', 'active'] },
popups: { rw: true, fields: ['title', 'type(newsletter|discount|announcement|exit)', 'headline', 'body', 'image', 'cta_text', 'cta_url', 'trigger', 'trigger_value', 'target_path', 'freq', 'active', 'sort', 'style(modal|slidein|bar)', 'discount_id'] },
discounts: { rw: true, fields: ['code', 'title', 'type(percent|fixed|freeshipping)', 'value', 'min_order_cents', 'starts_at', 'expires_at', 'max_uses', 'used_count', 'max_per_customer', 'active', 'secret', 'auto'] },
shipping_zones: { rw: true, fields: ['name', 'countries(CSV ISO, EU)', 'price_cents', 'free_over_cents', 'delivery_days', 'sort', 'active'] },
settings: { rw: true, fields: ['key/value-Map (shop_name, brand_accent, currency, free_shipping_cents, …)'] },
orders: { rw: false, fields: ['number', 'email', 'customer_name', 'status', 'total_cents', 'tax_cents', 'shipping_cents', 'country', 'items[]', 'address', 'created_at'] },
customers: { rw: false, fields: ['name', 'email', 'city', 'orders_count', 'total_spent_cents', 'created_at'] },
};
export function listResource(name) {
switch (name) {
case 'products': return store.listProducts();
case 'pages': return store.listPages();
case 'slides': return store.listSlides();
case 'popups': return store.listPopups();
case 'discounts': return store.listDiscounts();
case 'shipping_zones': return store.listShippingZones();
case 'orders': return store.listOrders();
case 'customers': return store.listCustomers();
case 'settings': return store.getSettings();
default: return null;
}
}
export function getResource(name, id) {
switch (name) {
case 'products': return store.getProductById(id);
case 'pages': return /^\d+$/.test(String(id)) ? store.getPageById(id) : store.getPageBySlug(id);
case 'slides': return store.getSlideById(id);
case 'popups': return store.getPopupById(id);
case 'discounts': return store.getDiscountById(id);
case 'shipping_zones': return store.getShippingZoneById(id);
case 'orders': return store.getOrderById(id);
case 'customers': return store.getCustomerById(id);
default: return null;
}
}
// upsert: bei id -> update, sonst create. Für products/pages erlaubt auch slug als Schlüssel.
export function upsertResource(name, body) {
if (name === 'products') {
if (body.id) { store.updateProduct(body.id, body); return { id: Number(body.id), ...store.getProductById(body.id) }; }
if (body.slug) { const ex = store.getProductBySlug(body.slug); if (ex) { store.updateProduct(ex.id, { ...ex, ...body }); return store.getProductById(ex.id); } }
const id = store.createProduct(body); return store.getProductById(id);
}
if (name === 'pages') {
if (body.id) { store.updatePage(body.id, body); return store.getPageById(body.id); }
if (body.slug) { const ex = store.getPageBySlug(body.slug); if (ex) { store.updatePage(ex.id, { ...ex, ...body }); return store.getPageById(ex.id); } }
const id = store.createPage(body); return store.getPageById(id);
}
if (name === 'slides') {
if (body.id) { store.updateSlide(body.id, body); return store.getSlideById(body.id); }
const id = store.createSlide(body); return store.getSlideById(id);
}
if (name === 'popups') {
if (body.id) { store.updatePopup(body.id, body); return store.getPopupById(body.id); }
const id = store.createPopup(body); return store.getPopupById(id);
}
if (name === 'discounts') {
if (body.id) { store.updateDiscount(body.id, body); return store.getDiscountById(body.id); }
if (body.code) { const ex = store.getDiscountByCode(body.code); if (ex) { store.updateDiscount(ex.id, { ...ex, ...body }); return store.getDiscountById(ex.id); } }
const id = store.createDiscount(body); return store.getDiscountById(id);
}
if (name === 'shipping_zones') {
if (body.id) { store.updateShippingZone(body.id, body); return store.getShippingZoneById(body.id); }
const id = store.createShippingZone(body); return store.getShippingZoneById(id);
}
if (name === 'settings') {
const entries = body && typeof body === 'object' ? Object.entries(body) : [];
for (const [k, v] of entries) store.setSetting(k, v);
return store.getSettings();
}
throw new Error('Ressource nicht schreibbar: ' + name);
}
export function deleteResource(name, id) {
switch (name) {
case 'products': store.deleteProduct(id); return true;
case 'pages': store.deletePage(id); return true;
case 'slides': store.deleteSlide(id); return true;
case 'popups': store.deletePopup(id); return true;
case 'discounts': store.deleteDiscount(id); return true;
case 'shipping_zones': store.deleteShippingZone(id); return true;
default: throw new Error('Ressource nicht löschbar: ' + name);
}
}
export function updatePageBlocks(id, blocks) { store.updatePageBlocks(id, blocks); return store.getPageById(id); }
export function recordAudit(o) { store.recordAudit(o); }
export function blockTypes() { return BLOCK_TYPES.map(b => ({ key: b.key, label: b.label, fields: b.fields.map(f => ({ name: f.name, type: f.type })) })); }
export function manifest(origin) {
const ep = [];
ep.push({ method: 'GET', path: '/api/admin', desc: 'Dieses Manifest' });
for (const [name, def] of Object.entries(RESOURCES)) {
ep.push({ method: 'GET', path: `/api/admin/${name}`, desc: `Liste ${name}` });
ep.push({ method: 'GET', path: `/api/admin/${name}/{id}`, desc: `Einzelnes ${name}` });
if (def.rw) {
ep.push({ method: 'POST', path: `/api/admin/${name}`, desc: `Upsert ${name} (id oder slug => Update, sonst Create)` });
if (name !== 'settings') ep.push({ method: 'DELETE', path: `/api/admin/${name}/{id}`, desc: `Löschen ${name}` });
}
}
ep.push({ method: 'POST', path: '/api/admin/pages/{id}/blocks', desc: 'Block-Array einer Seite setzen' });
return {
name: 'hd-commerce Admin API',
version: '2.2.0',
auth: 'Authorization: Bearer <HDC_API_TOKEN>',
base_url: origin || '',
resources: RESOURCES,
block_types: blockTypes(),
endpoints: ep,
notes: [
'Preise in Cent (priceCents/total_cents).',
'orders und customers sind nur lesbar.',
'settings ist eine Key/Value-Map; POST mit beliebigen Keys aktualisiert sie.',
'pages.blocks ist ein Array von Blöcken (siehe block_types) für den Visual-Builder.',
'Block-Objekte sind FLACH: { type, <feldname>: ... } — NICHT unter einem data-Schlüssel verschachtelt.',
'discounts.value: bei percent 1100, bei fixed in Cent, bei freeshipping ignoriert. Codes werden case-insensitiv geprüft.',
'shipping_zones.countries: CSV von ISO-Codes (DE, AT,CH) oder "EU" für alle EU-Länder. free_over_cents nullable.',
'products.mwst: 0, 7 oder 19. base_amount/base_unit/base_price_per ergeben den Grundpreis (PAngV), z. B. 250 + g + kg.',
'Feature-Flags & payment_provider sind Settings-Keys (über /api/admin/settings setzbar): feature_newsletter, feature_accounts, …, payment_provider (mollie|stripe|demo).',
],
};
}