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 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-05 09:37:29 +00:00
1 parent 21805c9a93
commit c6706f88ff
6 files changed
+42 -15

No files matched your search

+5 -3
View File
@@ -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',
+2 -1
View File
@@ -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,
+5 -3
View File
@@ -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) })
}))
+18 -5
View File
@@ -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 () => {
+10 -2
View File
@@ -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')
+2 -1
View File
@@ -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