From c6706f88ff7ffb6624d29c19698181c550eb2532 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 09:37:29 +0000 Subject: [PATCH] Shop Review A M2: Bestellaktionen, Erstattung und Widerruf erledigt auch bei abgeschaltetem Shop Die Routen aktion, erstatten und widerrufe/erledigt akzeptieren jetzt auch den Zustand disabled; Einstellungen, Artikel und Oeffnen bleiben gesperrt. Die Oberflaeche laesst Bestellungen und Widerrufe bei abgeschaltetem Shop bearbeitbar (ctx.abwickeln), der Hinweis nennt es. Co-Authored-By: Claude Opus 5.5 --- public/shop-tab-bestellungen.js | 8 +++++--- public/shop.js | 3 ++- server/routes/shop.js | 8 +++++--- test/shopBestellAktionen.test.js | 23 ++++++++++++++++++----- test/shopTabBestellungen.test.js | 12 ++++++++++-- test/shopView.test.js | 3 ++- 6 files changed, 42 insertions(+), 15 deletions(-) diff --git a/public/shop-tab-bestellungen.js b/public/shop-tab-bestellungen.js index 1822e1a..da88d91 100644 --- a/public/shop-tab-bestellungen.js +++ b/public/shop-tab-bestellungen.js @@ -5,7 +5,7 @@ Erstatten (POST /bestellungen/:id/erstatten { projectId, betragCent, grund? }) nur nach Rückfrage in der Seite. Darunter der CSV-Export (GET /api/shop/bestellungen.csv?projectId=&von=&bis= als Link zum Herunterladen). - Widerrufe: Liste (GET /api/shop/widerrufe?projectId=&seite=) mit „Als erledigt markieren“ (POST /widerrufe/:id/erledigt { projectId }). - Mit ctx.nurLesen (Shop abgeschaltet) gibt es keine Knöpfe zum Ändern. In der Kundenansicht antwortet der Server beim Ändern mit 403: der Text wird gezeigt. + Mit ctx.nurLesen (Shop abgeschaltet) gibt es keine Knöpfe zum Ändern, ausser ctx.abwickeln ist gesetzt (shop.js: Bestellungen und Widerrufe bleiben bearbeitbar). In der Kundenansicht antwortet der Server beim Ändern mit 403: der Text wird gezeigt. render(panel, ctx): ctx = { projectId, status, nurLesen, aktuell(), neuLaden(), zeigeMeldung(text, art), request } (shop.js). Fremdtext (Käufer, Artikel, Erklärung) kommt nur über textContent in die Seite. */ @@ -77,6 +77,8 @@ const fakten = (...zeilen) => h('dl', { class: 'sh-fakten' }, zeilen.filter(Bool export function render(panel, ctx) { const wurzel = h('div', { class: 'sh-bestellungen' }) panel.replaceChildren(wurzel) + // Bei abgeschaltetem Shop bleiben Bestellungen, Erstattungen und Widerrufe bearbeitbar (ctx.abwickeln), alles andere ist nur lesbar + const nurLesen = ctx.nurLesen === true && ctx.abwickeln !== true const z = { ansicht: 'bestellungen', filter: 'bezahlt', suche: '', seite: 1, widerrufSeite: 1, detail: null } let lauf = 0 // nur die zuletzt gestartete Abfrage zeichnet let beschaeftigt = false @@ -298,7 +300,7 @@ export function render(panel, ctx) { ))) // Aktionen - if (!ctx.nurLesen && (aktionen.length || b.kannErstatten)) teile.push(aktionenKarte(b, aktionen)) + if (!nurLesen && (aktionen.length || b.kannErstatten)) teile.push(aktionenKarte(b, aktionen)) // Verlauf und Erstattungen const verlauf = Array.isArray(b.verlauf) ? b.verlauf.filter(istObjekt) : [] @@ -461,7 +463,7 @@ export function render(panel, ctx) { ), h('div', { class: 'sh-aktionen' }, w.orderId ? h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'zur-bestellung', onclick: () => { z.ansicht = 'bestellungen'; oeffneDetail(w.orderId) } }, 'Zur Bestellung') : null, - !erledigt && !ctx.nurLesen ? h('button', { + !erledigt && !nurLesen ? h('button', { type: 'button', class: 'kb-btn-primary', 'data-aktion': 'erledigt', diff --git a/public/shop.js b/public/shop.js index 766962d..444b5ce 100644 --- a/public/shop.js +++ b/public/shop.js @@ -27,7 +27,7 @@ export const REITER = [ ] const HINWEIS_SETUP = 'Wir bauen deinen Shop gerade ein. Du kannst schon Artikel, Stripe und Rechtstexte vorbereiten.' -const HINWEIS_ABGESCHALTET = 'Dein Webshop ist abgeschaltet. Du kannst Bestellungen noch ansehen und exportieren.' +const HINWEIS_ABGESCHALTET = 'Dein Webshop ist abgeschaltet. Du kannst Bestellungen noch ansehen, bearbeiten, erstatten und exportieren.' const DANKE_SETUP = 'Danke! Die Zahlung ist bei uns angekommen.' const NICHT_VERFUEGBAR = 'Der Webshop ist noch nicht verfügbar.' const SHOP_PREIS = '299 €' @@ -309,6 +309,7 @@ function zeigeReiter(s, { status, aktuell, zustand }) { projectId: s.projekt.projectId, status, nurLesen: zustand === 'disabled', + abwickeln: true, // Bestellungen, Erstattungen und Widerrufe gehen auch bei abgeschaltetem Shop (Pflichten gegenueber Kaeufern) stripeRueckkehr: s.stripe, aktuell, request, diff --git a/server/routes/shop.js b/server/routes/shop.js index 3460a0f..6838e7c 100644 --- a/server/routes/shop.js +++ b/server/routes/shop.js @@ -536,6 +536,8 @@ const seiteVon = (wert) => { const textParam = (wert) => (typeof wert === 'string' ? wert : undefined) /** Status aendern verschickt Mails an Kaeufer und erstattet Geld: nie in der Kundenansicht oder "Ansehen als". */ const nurHaendler = nurKunde +/** Bestellungen bearbeiten, erstatten und Widerrufe abhaken muss auch bei deaktivierter Funktion gehen (Widerruf, Erstattung bleiben Pflicht). */ +const ZUSTAENDE_ABWICKLUNG = ['setup', 'active', 'disabled'] // Liste (Lesen auch in der Kundenansicht und bei deaktiviertem Shop) router.get('/bestellungen', requireSession, route(async (req, res) => { @@ -563,7 +565,7 @@ router.get('/bestellungen/:id', requireSession, route(async (req, res) => { })) router.post('/bestellungen/:id/aktion', requireSession, nurHaendler, limitWrites, route(async (req, res) => { - const { shop, doc, project } = await mitShop(req, { schreiben: true }) + const { shop, doc, project } = await mitShop(req, { schreiben: true, zustaende: ZUSTAENDE_ABWICKLUNG }) const aktion = typeof req.body?.aktion === 'string' ? req.body.aktion : '' const { order, mailOk } = await setzeStatus({ doc, shop, project }, req.params.id, aktion, { versanddienst: req.body?.versanddienst, @@ -573,7 +575,7 @@ router.post('/bestellungen/:id/aktion', requireSession, nurHaendler, limitWrites })) router.post('/bestellungen/:id/erstatten', requireSession, nurHaendler, limitWrites, route(async (req, res) => { - const { shop, doc, project } = await mitShop(req, { schreiben: true }) + const { shop, doc, project } = await mitShop(req, { schreiben: true, zustaende: ZUSTAENDE_ABWICKLUNG }) const { order, erstattung, voll, mailOk } = await erstatte({ doc, shop, project }, req.params.id, { betragCent: req.body?.betragCent, grund: req.body?.grund, @@ -587,7 +589,7 @@ router.get('/widerrufe', requireSession, route(async (req, res) => { })) router.post('/widerrufe/:id/erledigt', requireSession, nurHaendler, limitWrites, route(async (req, res) => { - const { shop } = await mitShop(req, { schreiben: true }) + const { shop } = await mitShop(req, { schreiben: true, zustaende: ZUSTAENDE_ABWICKLUNG }) res.json({ widerruf: await markiereBearbeitet(shop.id, req.params.id) }) })) diff --git a/test/shopBestellAktionen.test.js b/test/shopBestellAktionen.test.js index 6f9b989..fb062cb 100644 --- a/test/shopBestellAktionen.test.js +++ b/test/shopBestellAktionen.test.js @@ -565,19 +565,32 @@ test('Routen: CSV als Download mit BOM, Dateiname und Zeitraum; ungültiges Datu assert.equal((await send('GET', `/api/shop/bestellungen.csv?projectId=${PROJEKT}`)).status, 401) }) -test('Routen: deaktivierter Shop - Lesen und CSV gehen, Schreiben 409', async () => { +test('Routen M2: deaktivierter Shop - Lesen, CSV, Status ändern, Erstatten und „Widerruf erledigt“ gehen weiter; Einstellungen und Artikel bleiben gesperrt', async () => { const id = bestellung() + aw.seed('shopWiderrufe', { $id: 'w1', shopId, customerId: KUNDE, bestellnummer: '26-A', name: 'Alt', email: 'alt@example.com', nachricht: '', eingangAt: nach(1).toISOString() }) await updateDocument(config.collections.portalFeatures, feature.$id, sf.stateFields('disabled')) assert.equal((await lesen('bestellungen')).status, 200) assert.equal((await lesen(`bestellungen/${id}`)).status, 200) assert.equal((await lesen('bestellungen.csv')).status, 200) assert.equal((await lesen('widerrufe')).status, 200) - for (const [pfad, body] of [[`bestellungen/${id}/aktion`, { aktion: 'abgeschlossen' }], [`bestellungen/${id}/erstatten`, {}], ['widerrufe/w1/erledigt', {}]]) { - const res = await schreiben(pfad, body) + + const versand = await schreiben(`bestellungen/${id}/aktion`, { aktion: 'versendet' }) + assert.equal(versand.status, 200) + assert.equal(order(id).status, 'versendet') + const teil = await schreiben(`bestellungen/${id}/erstatten`, { betragCent: 300 }) + assert.deepEqual([teil.status, teil.body.voll], [200, false]) + assert.equal(refunds().length, 1) + const rest = await schreiben(`bestellungen/${id}/erstatten`, {}) + assert.deepEqual([rest.status, rest.body.voll], [200, true]) + const erledigt = await schreiben('widerrufe/w1/erledigt') + assert.equal(erledigt.status, 200) + assert.ok(erledigt.body.widerruf.bearbeitetAt) + + // Alles andere, was verwaltet oder geoeffnet wird, bleibt gesperrt + for (const [methode, pfad, body] of [['POST', 'produkte', { produkt: { name: 'X', preisCent: 100 } }], ['PUT', 'einstellungen', { einstellungen: {} }], ['POST', 'oeffnen', { offen: true }]]) { + const res = await send(methode, `/api/shop/${pfad}`, { cookie, body: { projectId: PROJEKT, ...body } }) assert.deepEqual([res.status, res.body.code], [409, 'nicht_eingerichtet'], pfad) } - assert.equal(order(id).status, 'bezahlt') - assert.equal(refunds().length, 0) }) test('Routen: Widerrufe - Liste (neueste zuerst, nur eigener Shop) und als erledigt markieren', async () => { diff --git a/test/shopTabBestellungen.test.js b/test/shopTabBestellungen.test.js index 5513d66..219cb63 100644 --- a/test/shopTabBestellungen.test.js +++ b/test/shopTabBestellungen.test.js @@ -39,9 +39,9 @@ const widerruf = (nr, extra = {}) => ({ /** Fake-Server: jede Anfrage geht an route(aufruf) ⇒ { status, body }; Standard 200 {}. */ const starte = (route) => installFakeDom({ antworten: Array.from({ length: 60 }, () => route) }) -function baueCtx({ nurLesen = false } = {}) { +function baueCtx({ nurLesen = false, abwickeln } = {}) { const ctx = { - projectId: 'p1', status: { state: 'active' }, nurLesen, meldungen: [], aktuell: () => true, request, + projectId: 'p1', status: { state: 'active' }, nurLesen, ...(abwickeln ? { abwickeln } : {}), meldungen: [], aktuell: () => true, request, zeigeMeldung: (text, art) => ctx.meldungen.push({ text, art }), neuLaden: async () => ctx.status, } @@ -496,6 +496,14 @@ test('nurLesen: keine Aktionen, kein Erstatten, kein „erledigt“; Lesen und C assert.equal(posts(fx).length, 0) })) +test('abwickeln (Shop abgeschaltet): Aktionen, Erstatten und „erledigt“ bleiben, obwohl nurLesen gilt', mit(server(), async (fx) => { + const { panel } = await oeffneDetail(fx, { nurLesen: true, abwickeln: true }) + assert.ok(aktion(fx, panel, 'versendet') || aktion(fx, panel, 'abgeschlossen') || fx.finde(panel, 'form[data-form="erstatten"]')) + await fx.klick(fx.finde(panel, '[data-ansicht="widerrufe"]')) + await fx.ruhe() + assert.ok(aktion(fx, panel, 'erledigt')) +})) + test('Antwort nach dem Wechsel des Reiters zeichnet nicht in das fremde Panel', mit(server(), async (fx) => { const ctx = baueCtx() const panel = h('div') diff --git a/test/shopView.test.js b/test/shopView.test.js index e7577f1..59525b5 100644 --- a/test/shopView.test.js +++ b/test/shopView.test.js @@ -431,8 +431,9 @@ test('Zustand disabled: Banner der Abschaltung, ctx.nurLesen = true', mitDom(wel REITER[0].laden = async () => ({ render: (panel, ctx) => { gesehen.push(ctx) } }) try { const root = await oeffne(fx) - assert.equal(fx.text(fx.finde(root, '[data-hinweis="disabled"]')), 'Dein Webshop ist abgeschaltet. Du kannst Bestellungen noch ansehen und exportieren.') + assert.equal(fx.text(fx.finde(root, '[data-hinweis="disabled"]')), 'Dein Webshop ist abgeschaltet. Du kannst Bestellungen noch ansehen, bearbeiten, erstatten und exportieren.') assert.equal(gesehen[0].nurLesen, true) + assert.equal(gesehen[0].abwickeln, true, 'Bestellungen und Widerrufe lassen sich auch bei abgeschaltetem Shop abwickeln (M2)') assert.equal(fx.alle(root, '[role="tab"]').length, 5) } finally { REITER[0].laden = alt