Webshop A3: Register-Eintrag shop, Haken, Rechtstext-Satz und Ticket-Details

Registereintrag config.features.shop (Bestelltexte mit Fassung config.shop.fassung, Rechtsunterlagen-Getter,
Mailtexte, Ticket), STANDARD_HAKEN.shop (Schalter, Bestellpruefung ueber shop/anfrage.js, Unterlagen, Belehrung),
legalDocs-Satz shop, shopTicketDetails sowie Rumpf von anfrageOffenePunkte. Tests angepasst und um Shop-Faelle erweitert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-05 08:35:28 +00:00
1 parent 6782d99960
commit 0e74f1669c
12 files changed
+594 -23

No files matched your search

+29
View File
@@ -0,0 +1,29 @@
import { config } from '../../config.js'
import { getDocument } from '../appwriteAdmin.js'
import { getFeatureDoc } from '../siteFeatures.js'
/**
* Webshop-Anfrage (Spezifikation 2026-10-05-webshop-design.md, Abschnitt 4 "Auf Anfrage"). Dieser Stand ist der Rumpf aus
* Task A3: nur die Bestellpruefung; Block B (B1, B2) baut Anfrage stellen, zurueckziehen und die Team-Entscheidung aus.
* Die Anfrage steht im Dokument shopConfig, dessen ID gleich der ID des portalFeatures-Dokuments des Projekts ist.
*/
export const ANFRAGE_OFFEN_TEXT = 'Deine Webshop-Anfrage ist noch nicht freigegeben.'
/**
* Offene Punkte der Bestellpruefung (featureHaken('shop').bestellpruefung): [] wenn die Anfrage des Projekts vom Team angenommen
* ist (anfrageStatus 'angenommen'), sonst ein Punkt. Ohne portalFeatures-Dokument, ohne shopConfig-Dokument oder bei jedem anderen
* Status ist die Anfrage nicht freigegeben. Lesefehler werden weitergereicht (der Checkout bestellt dann nicht).
*/
export async function anfrageOffenePunkte({ customerId, projectId }) {
const doc = await getFeatureDoc(customerId, projectId, 'shop')
if (!doc) return [ANFRAGE_OFFEN_TEXT]
let shop
try {
shop = await getDocument(config.collections.shopConfig, doc.$id)
} catch (err) {
if (err?.status === 404 && err.type !== 'collection_not_found' && err.type !== 'database_not_found') return [ANFRAGE_OFFEN_TEXT]
throw err
}
return shop?.anfrageStatus === 'angenommen' ? [] : [ANFRAGE_OFFEN_TEXT]
}
+69
View File
@@ -0,0 +1,69 @@
import { config } from '../../config.js'
import { getDocument } from '../appwriteAdmin.js'
import { TICKET_DETAILS_MAX, anleitungsLink, kuerze, minimaleTicketDetails } from '../featureRegistry.js'
/**
* Details des Einrichtungs-Tickets fuer den Webshop (config.features.shop.ticket.details, Spezifikation Abschnitt 6).
* ctx = { feature, doc, project, customer } wie ihn markPaid uebergibt (doc = portalFeatures-Dokument, project = websiteProjects-
* Dokument, customer = Kunde oder null). Der Text ist deterministisch: Kopfzeilen, Kurzfassung der Anfrage (best effort aus
* shopConfig, Dokument-ID = ID des portalFeatures-Dokuments; Lesefehler oder unlesbares JSON => ohne), Snippet und Schritte.
* Die Funktion wirft nie (bezahlt ist bezahlt, das Ticket entsteht immer) und enthaelt keine Daten von Kaeufern.
*/
const WARENART = { waren: 'Waren', lebensmittel: 'Lebensmittel', kleidung: 'Kleidung', kunst: 'Kunst', sonstiges: 'Sonstiges' }
const ARTIKELZAHL = { bis20: 'bis 20', bis100: 'bis 100', mehr: 'mehr als 100' }
const istObjekt = (wert) => wert !== null && typeof wert === 'object' && !Array.isArray(wert)
/** Kurzfassung der Anfrage als Zeile, oder '' (Lesefehler, fehlendes Dokument, leere Anfrage). */
async function anfrageZeile(doc) {
try {
const shop = await getDocument(config.collections.shopConfig, doc.$id)
const anfrage = typeof shop?.anfrage === 'string' && shop.anfrage ? JSON.parse(shop.anfrage) : null
if (!istObjekt(anfrage)) return ''
const teile = []
if (Object.hasOwn(WARENART, anfrage.warenart)) teile.push(`Warenart: ${WARENART[anfrage.warenart]}`)
const lieferung = [anfrage.versand === true ? 'Versand' : '', anfrage.abholung === true ? 'Abholung' : ''].filter(Boolean)
if (lieferung.length) teile.push(`Lieferung: ${lieferung.join(' und ')}`)
if (Object.hasOwn(ARTIKELZAHL, anfrage.artikelzahl)) teile.push(`Artikelzahl: ${ARTIKELZAHL[anfrage.artikelzahl]}`)
return teile.length ? `Anfrage: ${teile.join(', ')}` : ''
} catch {
return ''
}
}
export async function shopTicketDetails(ctx) {
try {
const { doc } = ctx
const siteKey = doc?.siteKey || ''
const kopf = minimaleTicketDetails({ ...ctx, feature: { key: 'Webshop' } }).split('\n').filter((zeile) => !zeile.startsWith('Verweis: '))
const anfrage = doc?.$id ? await anfrageZeile(doc) : ''
const zeilen = [
...kopf,
...(anfrage ? [anfrage] : []),
'',
'Snippet (an der Stelle der Shop-Seite einsetzen):',
`<div data-webklar-shop="${siteKey}"></div>`,
'<script src="https://project.webklar.com/embed/shop.js" defer></script>',
'',
'Schritte:',
'1. Shop-Seite und Menüpunkt anlegen, Snippet einsetzen.',
`2. Auf allen Seiten im Footer den Link "Vertrag widerrufen" setzen: https://project.webklar.com/shop/${siteKey}/widerruf`,
'3. Datenschutz-Absatz Webshop einfügen (docs/funktionen/shop-datenschutz-absatz.md).',
'4. Impressum prüfen.',
`5. Vorschau prüfen: https://project.webklar.com/shop/${siteKey}`,
'6. Ticket schließen: das schaltet die Funktion frei.',
'',
'Zeit im Arbeitsblatt erfassen, nicht auf Abo-Stunden anrechnen.',
`Verweis: ${anleitungsLink(doc?.customerId, doc?.projectId)}`,
]
return kuerze(zeilen.join('\n').toWellFormed(), TICKET_DETAILS_MAX, { anZeile: true })
} catch (err) {
console.error(`[shop-ticket] Details nicht erzeugbar, Notfall-Details: ${err?.message}`)
try {
return kuerze(minimaleTicketDetails({ ...ctx, feature: { key: 'Webshop' } }).toWellFormed(), TICKET_DETAILS_MAX)
} catch {
return 'Funktion: Webshop\nDetails konnten nicht erzeugt werden, im WOMS öffnen (Kunden → Funktionen).'
}
}
}