Kundenansicht P6: Ticket-Texte zum Stundenkonto je Abrechnung (Abo, Projekt)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
df53cd039b
commit
bb1efa5c9e
5 files changed
+80
-7
No files matched your search
@@ -54,7 +54,8 @@ export async function shopTicketDetails(ctx) {
|
||||
`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.',
|
||||
// Abo-Modell (einrichtungAbrechnung abo oder projekt): die Abrechnung steht allein in der Schlusszeile des Tickets (createFeatureTicket)
|
||||
...(doc?.einrichtungAbrechnung === 'abo' || doc?.einrichtungAbrechnung === 'projekt' ? [] : ['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 })
|
||||
|
||||
@@ -488,6 +488,19 @@ const DATENSCHUTZ_REGEL =
|
||||
'liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügen wir den Absatz beim Einbau ein, das gehört zur Einrichtung und wird nicht auf das Stundenkontingent angerechnet; '
|
||||
+ 'sonst den Absatz als Textbaustein per Chat an den Kunden senden, der ihn selbst einfügt'
|
||||
|
||||
/**
|
||||
* Abrechnung am Dokument (einrichtungAbrechnung, Abo-Modell der Kundenansicht neu): 'abo' (aufs Abo-Stundenkonto) oder 'projekt' (im Projekt
|
||||
* enthalten); alles andere (Kauf-Bestand, Pilot, Kulanz) ist '' und behaelt die bisherigen Texte Zeichen fuer Zeichen.
|
||||
*/
|
||||
const abrechnungVon = (feature) => (feature?.einrichtungAbrechnung === 'abo' || feature?.einrichtungAbrechnung === 'projekt' ? feature.einrichtungAbrechnung : '')
|
||||
|
||||
/** DATENSCHUTZ_REGEL je Abrechnung: ohne Marker die Konstante wörtlich, im Abo-Modell ohne "nicht angerechnet". */
|
||||
function datenschutzRegel(feature) {
|
||||
const abrechnung = abrechnungVon(feature)
|
||||
if (!abrechnung) return DATENSCHUTZ_REGEL
|
||||
return DATENSCHUTZ_REGEL.replace('wird nicht auf das Stundenkontingent angerechnet', abrechnung === 'abo' ? 'zählt mit zum Stundenkonto' : 'ist im Projekt enthalten')
|
||||
}
|
||||
|
||||
function abschnittCheckliste(d) {
|
||||
const { e } = d
|
||||
const widerruf = zeigtWiderruf(e)
|
||||
@@ -501,7 +514,7 @@ function abschnittCheckliste(d) {
|
||||
'Einbau-Code eingefügt (Abschnitt „Einbau-Code (Snippet)“). Das Widget bleibt unsichtbar bis zur Freischaltung: Bis dahin steht nur der Ausweichtext auf der Seite.',
|
||||
...(knopf ? ['Modus knopf: --wkt-akzent immer setzen (Abschnitt „Aussehen (CSS)“). Der Knopf kennt die Betriebsfarbe erst nach dem Klick und nimmt sonst #1d4ed8.'] : []),
|
||||
'Ausweichtext geprüft: JavaScript aus oder falscher siteKey: Der Text steht, nichts ist kaputt.',
|
||||
`Datenschutz-Absatz „Online-Terminbuchung“ steht in der Datenschutzerklärung der Website (Fassung ${e.anrede === 'du' ? 'Du' : 'Sie'}; ${DATENSCHUTZ_REGEL}).`,
|
||||
`Datenschutz-Absatz „Online-Terminbuchung“ steht in der Datenschutzerklärung der Website (Fassung ${e.anrede === 'du' ? 'Du' : 'Sie'}; ${datenschutzRegel(d.feature)}).`,
|
||||
`URLs von Impressum (${impressum}) und Datenschutzerklärung (${datenschutz}) stimmen und beginnen mit https://, sonst fehlen die Links im Fuß des Widgets.`,
|
||||
`„Freischalten (Abnahme)“ im Ticketsystem ausgelöst${wo}. Erst danach antwortet die öffentliche Schnittstelle (nur im Zustand active), und das Widget erscheint.`,
|
||||
// am sichtbaren Widget
|
||||
@@ -531,6 +544,16 @@ function einrichtungsPreis() {
|
||||
}
|
||||
|
||||
function abschnittAbrechnung(d) {
|
||||
const abrechnung = abrechnungVon(d.feature)
|
||||
if (abrechnung) {
|
||||
// Abo-Modell: Einrichtung ohne Zahlung, die Zeit gehoert aufs Abo-Stundenkonto bzw. ins Projekt
|
||||
return neuerAbschnitt('abrechnung', 'Abrechnung', [
|
||||
abrechnung === 'abo'
|
||||
? 'Abrechnung: Einrichtung aus dem Abo, Zeit im Arbeitsblatt aufs Abo-Stundenkonto anrechnen (Vorschuss möglich).'
|
||||
: 'Abrechnung: im Projekt enthalten, Zeit im Arbeitsblatt, nicht aufs Abo anrechnen.',
|
||||
'Leistungsumfang: ein Widget an einer Stelle, Gestaltung nach Fragebogen, Einbau; Mehrwünsche (zweites Widget, Knöpfe je Leistung, Sonderdesign) nach Stunden, vorher im Chat abstimmen.',
|
||||
].join('\n'))
|
||||
}
|
||||
if (d.feature?.unlockedByPurchase !== true) {
|
||||
return neuerAbschnitt('abrechnung', 'Abrechnung', 'Abrechnung: ohne Zahlung freigeschaltet (Pilot/Kulanz), keine Abrechnung.')
|
||||
}
|
||||
@@ -663,14 +686,15 @@ function kurzUmfang(k) {
|
||||
function kurzSchritte(k) {
|
||||
const form = !k.hatEinstellung ? 'Sie- oder Du-Form nach der Anrede der Gäste' : k.anrede === 'du' ? 'Du-Form' : 'Sie-Form'
|
||||
const bezahlt = k.feature?.unlockedByPurchase === true ? ` (Einrichtung bezahlt, ${einrichtungsPreis()})` : ''
|
||||
const abrechnung = abrechnungVon(k.feature)
|
||||
return [
|
||||
'Schritte:',
|
||||
'1. WOMS-Panel öffnen (Kunde → Reiter Funktionen → Anleitung des Projekts): dort stehen alle Einstellungen, die Kopierknöpfe und die Abnahme-Checkliste.',
|
||||
'2. Einbau-Code einfügen: den Code oben an der Stelle einsetzen, die der Kunde nennt (siehe Kundenwunsch), das Skript einmal pro Seite.',
|
||||
'3. Gestaltung und Kontrast: das CSS aus der Anleitung (Abschnitt „Aussehen (CSS)“) in die Website übernehmen und den Kontrast prüfen.',
|
||||
`4. Datenschutz-Absatz (${form}) aus der Anleitung in die Datenschutzerklärung der Website aufnehmen, bevor freigeschaltet wird (${DATENSCHUTZ_REGEL}).`,
|
||||
`4. Datenschutz-Absatz (${form}) aus der Anleitung in die Datenschutzerklärung der Website aufnehmen, bevor freigeschaltet wird (${datenschutzRegel(k.feature)}).`,
|
||||
'5. „Freischalten (Abnahme)“ im WOMS auslösen, danach eine Probebuchung durchspielen und den Testtermin wieder stornieren (Checkliste in der Anleitung).',
|
||||
`6. Ticket erledigen und die Zeit im Arbeitsblatt buchen. Nicht auf das Stundenkontingent anrechnen${bezahlt}.`,
|
||||
abrechnung ? '6. Ticket erledigen und die Zeit im Arbeitsblatt buchen.' : `6. Ticket erledigen und die Zeit im Arbeitsblatt buchen. Nicht auf das Stundenkontingent anrechnen${bezahlt}.`,
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
|
||||
@@ -36,13 +36,15 @@ export async function termineTicketDetails(ctx) {
|
||||
|
||||
const ANLEITUNG_FEHLT = 'Anleitung konnte nicht erzeugt werden, im WOMS öffnen (Verweis oben).'
|
||||
const KONTINGENT = 'Nicht auf das Stundenkontingent anrechnen.'
|
||||
// Abo-Modell (Kundenansicht neu, P6): die Abrechnung steht allein in der Schlusszeile des Tickets (createFeatureTicket, ABRECHNUNGS_ZEILEN)
|
||||
const imAboModell = (doc) => doc?.einrichtungAbrechnung === 'abo' || doc?.einrichtungAbrechnung === 'projekt'
|
||||
|
||||
/** Kopfzeilen und Verweis (minimaleTicketDetails, Funktion wie im Kopf der Kurzfassung) plus Hinweis; wirft nie, auch nicht bei einem unbrauchbaren ctx. */
|
||||
function notfallDetails(ctx) {
|
||||
try {
|
||||
const kopf = minimaleTicketDetails({ ...ctx, feature: { key: 'Terminplaner' } })
|
||||
return kuerze(`${kopf}\n${ANLEITUNG_FEHLT}\n${KONTINGENT}`.toWellFormed(), TICKET_DETAILS_MAX)
|
||||
return kuerze(`${kopf}\n${ANLEITUNG_FEHLT}${imAboModell(ctx?.doc) ? '' : `\n${KONTINGENT}`}`.toWellFormed(), TICKET_DETAILS_MAX)
|
||||
} catch {
|
||||
return `Funktion: Terminplaner\nAnleitung konnte nicht erzeugt werden, im WOMS öffnen (Kunden → Funktionen).\n${KONTINGENT}`
|
||||
return `Funktion: Terminplaner\nAnleitung konnte nicht erzeugt werden, im WOMS öffnen (Kunden → Funktionen).${imAboModell(ctx?.doc) ? '' : `\n${KONTINGENT}`}`
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,8 @@ export async function unterkunftTicketDetails(ctx) {
|
||||
'5. Testanfrage über den Kalender stellen und im Portal (Buchungen) prüfen.',
|
||||
'6. Ticket schließen: das schaltet die Funktion frei (sobald der Kunde Unterkunft, Preise, Stornobedingungen und Firmendaten hinterlegt hat).',
|
||||
'',
|
||||
'Zeit im Arbeitsblatt erfassen, nicht auf Abo-Stunden anrechnen.',
|
||||
// Abo-Modell (einrichtungAbrechnung abo oder projekt): die Abrechnung steht allein in der Schlusszeile des Tickets (createFeatureTicket)
|
||||
...(doc?.einrichtungAbrechnung === 'abo' || doc?.einrichtungAbrechnung === 'projekt' ? [] : ['Zeit im Arbeitsblatt erfassen, nicht auf Abo-Stunden anrechnen.']),
|
||||
'Einbauanleitung: docs/funktionen/unterkunft-einbau.md',
|
||||
`Verweis: ${anleitungsLink(doc?.customerId, doc?.projectId)}`,
|
||||
]
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { test, after } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { startAppwriteMock } from './support/appwriteMock.js'
|
||||
import { startFakeStripe } from './support/fakeStripe.js'
|
||||
import { useTestEnv } from './support/testEnv.js'
|
||||
|
||||
// Ticket-Texte zum Stundenkonto je Abrechnung (P6 Task 14): Dokumente des Abo-Modells (einrichtungAbrechnung abo oder projekt) sagen im
|
||||
// Ticket nirgends "nicht anrechnen"; die Abrechnung steht allein in der Schlusszeile aus createFeatureTicket (ABRECHNUNGS_ZEILEN).
|
||||
const aw = await startAppwriteMock()
|
||||
const st = await startFakeStripe()
|
||||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||||
after(() => Promise.all([aw.close(), st.close()]))
|
||||
const { termineTicketDetails } = await import('../server/services/termine/ticketDetails.js')
|
||||
const { shopTicketDetails } = await import('../server/services/shop/ticketDetails.js')
|
||||
const { unterkunftTicketDetails } = await import('../server/services/unterkunft/ticketDetails.js')
|
||||
const { einbauanleitung } = await import('../server/services/termine/einbauanleitung.js')
|
||||
const { ausVorlage } = await import('../server/services/termine/vorlagen.js')
|
||||
|
||||
const NOW = new Date('2026-10-17T08:00:00.000Z')
|
||||
// Salon-Vorlage als gespeicherte Einstellung: erst damit erzeugt der Generator alle Abschnitte (Checkliste, Abrechnung), ohne Einstellung nur das Snippet
|
||||
const EINSTELLUNG = () => ausVorlage('salon', { name: 'Salon Muster', email: 'inhaber@salon-muster.example', telefon: '0631 123456', adresse: 'Hauptstraße 1, 67655 Kaiserslautern', kleinunternehmer: false, impressumUrl: 'https://salon-muster.example/impressum', datenschutzUrl: 'https://salon-muster.example/datenschutz', bundesland: 'RP' }, NOW)
|
||||
const ALT = /nicht auf (das )?Stundenkontingent|nicht auf Abo-Stunden|angerechnet/i
|
||||
const ctx = (key, doc) => ({ feature: { key }, project: { projectName: 'Bäckerei', subdomain: 'baeckerei' }, customer: { name: 'Kunde' }, doc: { $id: `f_${key}`, siteKey: 'abcdefghijklmnop', customerId: 'c1', projectId: 'p1', featureKey: key, ...doc } })
|
||||
const BAUER = { termine: termineTicketDetails, shop: shopTicketDetails, unterkunft: unterkunftTicketDetails }
|
||||
for (const [key, bau] of Object.entries(BAUER)) {
|
||||
test(`${key}: mit Abrechnung abo und projekt keine „nicht anrechnen“-Aussage im Ticket`, async () => {
|
||||
for (const einrichtungAbrechnung of ['abo', 'projekt']) assert.doesNotMatch(await bau(ctx(key, { einrichtungAbrechnung, unlockedBy: einrichtungAbrechnung })), ALT, `${key} ${einrichtungAbrechnung}`)
|
||||
})
|
||||
test(`${key}: Kauf-Bestand und Altdokumente behalten ihren Wortlaut`, async () => { assert.match(await bau(ctx(key, key === 'termine' ? { unlockedByPurchase: true } : {})), ALT) })
|
||||
}
|
||||
test('Terminplaner-Anleitung: Text und Kurzfassung je Abrechnung; Notfalltext ohne Kontingent-Satz', async (t) => {
|
||||
t.mock.method(console, 'error', () => {})
|
||||
const gen = (feature) => einbauanleitung({ feature: { $id: 'f1', siteKey: 'abcdefghijklmnop', customerId: 'c1', projectId: 'p1', ...feature }, project: { projectName: 'B' }, einstellung: EINSTELLUNG(), fragebogen: null, version: 1, updatedAt: null, kunde: { name: 'K' }, now: NOW })
|
||||
for (const a of ['abo', 'projekt']) { const g = gen({ einrichtungAbrechnung: a, unlockedBy: a }); assert.doesNotMatch(g.text, ALT, a); assert.doesNotMatch(g.kurz, ALT, a) }
|
||||
assert.match(gen({ einrichtungAbrechnung: 'abo', unlockedBy: 'abo' }).text, /Einrichtung aus dem Abo, Zeit im Arbeitsblatt aufs Abo-Stundenkonto anrechnen \(Vorschuss möglich\)\./)
|
||||
assert.match(gen({ einrichtungAbrechnung: 'projekt', unlockedBy: 'projekt' }).text, /im Projekt enthalten, Zeit im Arbeitsblatt, nicht aufs Abo anrechnen\./)
|
||||
assert.match(gen({ unlockedByPurchase: true }).kurz, /Nicht auf das Stundenkontingent anrechnen/)
|
||||
// Ersatztexte der Datenschutz-Regel und Schritt 6
|
||||
assert.match(gen({ einrichtungAbrechnung: 'abo', unlockedBy: 'abo' }).kurz, /das gehört zur Einrichtung und zählt mit zum Stundenkonto/)
|
||||
assert.match(gen({ einrichtungAbrechnung: 'projekt', unlockedBy: 'projekt' }).kurz, /das gehört zur Einrichtung und ist im Projekt enthalten/)
|
||||
assert.match(gen({ einrichtungAbrechnung: 'abo', unlockedBy: 'abo' }).kurz, /6\. Ticket erledigen und die Zeit im Arbeitsblatt buchen\.(\n|$)/)
|
||||
// Notfalltext (Anleitung nicht erzeugbar, hier: leere $id, ladeStand wirft): im Abo-Modell ohne Kontingent-Satz, ohne Marker wie heute
|
||||
assert.doesNotMatch(await termineTicketDetails(ctx('termine', { einrichtungAbrechnung: 'abo', unlockedBy: 'abo', $id: '' })), ALT)
|
||||
assert.match(await termineTicketDetails(ctx('termine', { $id: '' })), /Nicht auf das Stundenkontingent anrechnen\./)
|
||||
})
|
||||
Reference in new issue
Block a user