v2.1: Gutschein-/Rabatt-Engine + editierbare gebrandete 404
Feature 1 — Rabatt-Engine (store-sqlite.js): - Tabellen discounts + discount_redemptions; orders um discount_code/discount_cents erweitert. - Helper: getDiscountByCode, listDiscounts, create/update/deleteDiscount, validateDiscount (Zeitplan/Mindestwert/Limits/pro-Kunde), bestAutoDiscount, redeemDiscount. - Seed: WILLKOMMEN10, NAEHEN5, GRATISVERSAND (geplant), AUTO15AB75 (auto). - Checkout: /api/discount (serverseitige Subtotal-Berechnung) + /api/checkout re-validiert, wendet Rabatt/Gratisversand an, speichert + redeemt, auto-Discount-Fallback, Stripe-Coupon. - Cart/Checkout-UI mit Code-Feld + Einlösen; Rabattzeile in Order-Detail + Erfolgsseite. - Admin "Rabatte" (owner+redaktion) mit Status-Badges + Editor (Zufallscode, Typ-abh. Wertfeld). - Popups: Typ discount zeigt Code + Kopieren-Button; Stile modal/slidein/bar (CSS ergaenzt). Feature 2 — 404: - src/pages/404.astro nutzt Base + BlockRenderer, laedt System-Seite slug 404. - ensureSystemPages() legt 404 idempotent bei jedem Boot an (INSERT OR IGNORE, flache Bloecke). - 404/system erscheint in Admin "Inhalte" und oeffnet im Block-Editor. API/MCP: discounts in /api/admin/* (CRUD), Manifest + ai-admin.txt ergaenzt (inkl. Hinweis: Block-Objekte sind flach); MCP list/upsert/delete_discount. README + Versionen (2.1.0) aktualisiert.
This commit is contained in:
+13
-2
@@ -19,7 +19,8 @@ export const RESOURCES = {
|
||||
products: { rw: true, fields: ['slug', 'name', 'shortName', 'priceCents', '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', 'headline', 'body', 'image', 'cta_text', 'cta_url', 'trigger', 'trigger_value', 'target_path', 'freq', 'active', 'sort'] },
|
||||
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'] },
|
||||
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', 'items[]', 'address', 'created_at'] },
|
||||
customers: { rw: false, fields: ['name', 'email', 'city', 'orders_count', 'total_spent_cents', 'created_at'] },
|
||||
@@ -31,6 +32,7 @@ export function listResource(name) {
|
||||
case 'pages': return store.listPages();
|
||||
case 'slides': return store.listSlides();
|
||||
case 'popups': return store.listPopups();
|
||||
case 'discounts': return store.listDiscounts();
|
||||
case 'orders': return store.listOrders();
|
||||
case 'customers': return store.listCustomers();
|
||||
case 'settings': return store.getSettings();
|
||||
@@ -43,6 +45,7 @@ export function getResource(name, 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 'orders': return store.getOrderById(id);
|
||||
case 'customers': return store.getCustomerById(id);
|
||||
default: return null;
|
||||
@@ -69,6 +72,11 @@ export function upsertResource(name, body) {
|
||||
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 === 'settings') {
|
||||
const entries = body && typeof body === 'object' ? Object.entries(body) : [];
|
||||
for (const [k, v] of entries) store.setSetting(k, v);
|
||||
@@ -83,6 +91,7 @@ export function deleteResource(name, id) {
|
||||
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;
|
||||
default: throw new Error('Ressource nicht löschbar: ' + name);
|
||||
}
|
||||
}
|
||||
@@ -105,7 +114,7 @@ export function manifest(origin) {
|
||||
ep.push({ method: 'POST', path: '/api/admin/pages/{id}/blocks', desc: 'Block-Array einer Seite setzen' });
|
||||
return {
|
||||
name: 'hd-commerce Admin API',
|
||||
version: '2.0.0',
|
||||
version: '2.1.0',
|
||||
auth: 'Authorization: Bearer <HDC_API_TOKEN>',
|
||||
base_url: origin || '',
|
||||
resources: RESOURCES,
|
||||
@@ -116,6 +125,8 @@ export function manifest(origin) {
|
||||
'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 1–100, bei fixed in Cent, bei freeshipping ignoriert. Codes werden case-insensitiv geprüft.',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user