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:
1 parent
21805c9a93
commit
c6706f88ff
6 files changed
+42
-15
No files matched your search
@@ -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
@@ -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,
|
||||
|
||||
@@ -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) })
|
||||
}))
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user