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:
1 parent
6782d99960
commit
0e74f1669c
12 files changed
+594
-23
No files matched your search
@@ -1,9 +1,10 @@
|
||||
import { config } from '../config.js'
|
||||
import { termineFuerKunde } from './termine/schalter.js'
|
||||
import { shopFuerKunde } from './shop/schalter.js'
|
||||
|
||||
// Helfer fuer das Funktionen-Register config.features (News-Pinnwand, Terminplaner; Daten in server/config.js).
|
||||
// Diese Datei wird von siteFeatures.js importiert und importiert selbst keine Dienste statisch (einzige Ausnahme:
|
||||
// termine/schalter.js, das nur config liest): Standard-Haken laden ihre Dienste per dynamischem import()
|
||||
// Helfer fuer das Funktionen-Register config.features (News-Pinnwand, Terminplaner, Webshop; Daten in server/config.js).
|
||||
// Diese Datei wird von siteFeatures.js importiert und importiert selbst keine Dienste statisch (einzige Ausnahmen:
|
||||
// termine/schalter.js und shop/schalter.js, die nur config lesen): Standard-Haken laden ihre Dienste per dynamischem import()
|
||||
// innerhalb der Funktion (sonst entstuenden Importzyklen).
|
||||
|
||||
/** Namen der Haken, die ein Registereintrag hat (optional): Bestell-, Freischalt- und Unterlagenpruefungen. */
|
||||
@@ -58,6 +59,21 @@ async function termineBelehrung() {
|
||||
return widerrufsbelehrungFunktion()
|
||||
}
|
||||
|
||||
/**
|
||||
* Bestellpruefung des Webshops im Checkout: die Anfrage des Projekts muss vom Team angenommen sein (shop/anfrage.js,
|
||||
* Block B). Geladen werden die Dienste erst beim Aufruf.
|
||||
*/
|
||||
async function shopBestellpruefung({ customerId, projectId }) {
|
||||
const { anfrageOffenePunkte } = await import('./shop/anfrage.js')
|
||||
return anfrageOffenePunkte({ customerId, projectId })
|
||||
}
|
||||
|
||||
/** Vertragsunterlagen des Webshops abrufbar? (Stufe 5b des Checkouts; legalDocs.featureLegalReady, fail-closed, wirft nie.) */
|
||||
async function shopUnterlagenBereit() {
|
||||
const { featureLegalReady } = await import('./legalDocs.js')
|
||||
return featureLegalReady('shop')
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard-Haken je Funktion (Funktionen, nicht Daten). B6 fuellt verfuegbar, bestellpruefung und unterlagenBereit
|
||||
* (Terminplaner: featureLegalReady, F8), B7 belehrung (Text der Belehrung, der vor dem Knopf steht
|
||||
@@ -73,6 +89,13 @@ const STANDARD_HAKEN = {
|
||||
belehrung: termineBelehrung,
|
||||
freischaltbar: termineFreischaltbar,
|
||||
},
|
||||
// Webshop: kein freischaltbar (ein geschlossenes Ticket genuegt); Belehrung aus derselben Quelle wie beim Terminplaner
|
||||
shop: {
|
||||
verfuegbar: (email) => shopFuerKunde(email),
|
||||
bestellpruefung: shopBestellpruefung,
|
||||
unterlagenBereit: shopUnterlagenBereit,
|
||||
belehrung: termineBelehrung,
|
||||
},
|
||||
}
|
||||
|
||||
/** Registereintrag oder null; nur eigene Schluessel des Registers ('analyse', '__proto__', 'toString' => null). */
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Dokument-Registry der veroeffentlichten Rechtstexte (je Fassung feste Dateien unter public/rechtliches/):
|
||||
// Saetze 'analyse' (AVV, Leistungsbeschreibung der Website-Analyse) und 'termine' (Leistungsbeschreibung, AVV und
|
||||
// Widerrufsbelehrung mit Muster-Widerrufsformular des Terminplaners). analyseLegal.js ist ein duenner Wrapper darum.
|
||||
// Saetze 'analyse' (AVV, Leistungsbeschreibung der Website-Analyse), 'termine' und 'shop' (Leistungsbeschreibung, AVV und
|
||||
// Widerrufsbelehrung mit Muster-Widerrufsformular des Terminplaners bzw. Webshops). analyseLegal.js ist ein duenner Wrapper darum.
|
||||
// Die Fassung ist der Berliner Kalendertag der Texte (JJJJ-MM-TT); eine ausgelieferte Fassung wird nie ueberschrieben.
|
||||
import { access, open, readFile, stat } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
@@ -32,13 +32,22 @@ const SAETZE = {
|
||||
avv: { slug: 'avv-terminplaner', pdf: 'AVV-Terminplaner' },
|
||||
widerruf: { slug: 'widerrufsbelehrung-funktionen', pdf: 'Widerrufsbelehrung-Muster-Widerrufsformular' },
|
||||
},
|
||||
shop: {
|
||||
lb: { slug: 'leistungsbeschreibung-webshop', pdf: 'Leistungsbeschreibung-Webshop' },
|
||||
avv: { slug: 'avv-webshop', pdf: 'AVV-Webshop' },
|
||||
widerruf: { slug: 'widerrufsbelehrung-funktionen', pdf: 'Widerrufsbelehrung-Muster-Widerrufsformular' },
|
||||
},
|
||||
}
|
||||
|
||||
/** Satz, dessen Dokumente eine Funktion (Registerschluessel) mit der Bestellung bekommt; News hat keine. */
|
||||
const FUNKTION_SATZ = { news: null, termine: 'termine' }
|
||||
const FUNKTION_SATZ = { news: null, termine: 'termine', shop: 'shop' }
|
||||
|
||||
/** Aktuelle Fassung je Satz, bei jedem Aufruf aus config gelesen (Tests aendern sie zur Laufzeit). */
|
||||
const aktuelleFassung = (satz) => (satz === 'analyse' ? config.analyse.avvVersion : config.termine.avvVersion)
|
||||
const aktuelleFassung = (satz) => {
|
||||
if (satz === 'analyse') return config.analyse.avvVersion
|
||||
if (satz === 'shop') return config.shop.fassung
|
||||
return config.termine.avvVersion
|
||||
}
|
||||
|
||||
function satzDokumente(satz) {
|
||||
if (typeof satz !== 'string' || !Object.hasOwn(SAETZE, satz)) throw new Error('Rechtstexte: unbekannter Satz')
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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).'
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user