Kundenansicht P5: Setup-Skript für Kundenart und Einrichtungs-Marker
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c2b7073d99
commit
c5b1333953
4 files changed
+171
-4
No files matched your search
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Kundenansicht neu (Paket P5, Abo-Modell Kern): legt die neuen Attribute an. Der Code schreibt sie nur, wenn ein
|
||||
* Aufrufer die zugehoerigen Parameter uebergibt; ohne dieses Skript bleibt die alte Ansicht unberuehrt.
|
||||
* - featureConsents.kundenart: Kundenart zum Klickzeitpunkt im Nachweis ('privat' | 'unternehmen')
|
||||
* - workorders.einrichtungFuer: Schluessel der Funktion, die das Einrichtungs-Ticket betrifft (laengster: 'unterkunft')
|
||||
* - workorders.einrichtungAbrechnung: 'abo' | 'projekt' | '' (Abrechnung der Einrichtung am Ticket)
|
||||
* - portalFeatures.einrichtungAbrechnung: dieselbe Abrechnung am Funktionsdokument (Nachlegen und Heilen setzen
|
||||
* den Marker am Ticket erneut)
|
||||
* - portalFeatures.unlockedBy: 'kauf' | 'abo' | 'team' | 'projekt' (steht schon im Zeiten-Schema; fehlt es, legt
|
||||
* dieses Skript es an)
|
||||
* Idempotent: Vorhandenes wird uebersprungen. Das Skript loescht nichts und laeuft in Entwicklung und Tests nie
|
||||
* gegen Produktion; dort nur auf Anweisung des Controllers nach OK des Inhabers, vorher immer mit --pruefen.
|
||||
* scripts/setup-abo-collections.mjs, feature-setup.mjs und zeiten-setup.mjs bleiben unveraendert.
|
||||
* Usage:
|
||||
* node scripts/kundenansicht-setup.mjs --pruefen (Alias --check; nur melden, nichts anlegen; Exit 1 bei Luecken)
|
||||
* node scripts/kundenansicht-setup.mjs (Fehlendes anlegen)
|
||||
*
|
||||
* config.js und der Appwrite-Client werden erst in main() geladen: test/support/schema.js importiert die
|
||||
* Attributlisten, bevor die Tests ihre Umgebung (useTestEnv) setzen koennen.
|
||||
*/
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { ZEITEN_FEATURE_ATTRIBUTES } from '../server/services/zeiten/schema.js'
|
||||
|
||||
const str = (key, size) => ({ key, type: 'string', size, required: false })
|
||||
const unlockedBy = ZEITEN_FEATURE_ATTRIBUTES.find((a) => a.key === 'unlockedBy')
|
||||
|
||||
export const KUNDENANSICHT_ATTRIBUTE = {
|
||||
featureConsents: [str('kundenart', 12)],
|
||||
workorders: [str('einrichtungFuer', 16), str('einrichtungAbrechnung', 8)],
|
||||
portalFeatures: [unlockedBy, str('einrichtungAbrechnung', 8)],
|
||||
}
|
||||
|
||||
export async function main({ pruefen = false, client, log = console.log } = {}) {
|
||||
const { config } = await import('../server/config.js')
|
||||
let api = client
|
||||
if (!api) {
|
||||
const { createAppwriteClient } = await import('./analyse-setup.mjs')
|
||||
api = createAppwriteClient()
|
||||
}
|
||||
const missing = []
|
||||
const plan = []
|
||||
|
||||
// Erst alles pruefen, dann (ohne --pruefen) anlegen, damit --pruefen garantiert nichts veraendert
|
||||
for (const [name, attributes] of Object.entries(KUNDENANSICHT_ATTRIBUTE)) {
|
||||
const id = config.collections[name]
|
||||
const exists = await api.collectionExists(id)
|
||||
if (!exists && !pruefen) throw new Error(`Collection ${id} fehlt – zuerst das jeweilige Setup-Skript ausfuehren`)
|
||||
if (!exists) log(`- ${id} FEHLT`)
|
||||
const have = exists ? new Set((await api.listAttributes(id)).map((a) => a.key)) : new Set()
|
||||
for (const attr of attributes) {
|
||||
if (have.has(attr.key)) log(`= ${id}.${attr.key} (existiert)`)
|
||||
else if (pruefen) log(`- ${id}.${attr.key} FEHLT`)
|
||||
}
|
||||
const absent = attributes.filter((a) => !have.has(a.key))
|
||||
absent.forEach((a) => missing.push({ ...a, collection: id }))
|
||||
plan.push({ id, absent })
|
||||
}
|
||||
|
||||
const result = { missing }
|
||||
if (pruefen) {
|
||||
log(missing.length ? `${missing.length} Eintrag/Eintraege fehlen.` : 'Alles vorhanden.')
|
||||
return result
|
||||
}
|
||||
|
||||
for (const { id, absent } of plan) {
|
||||
for (const attr of absent) {
|
||||
await api.createAttribute(id, attr)
|
||||
log(`+ Attribut ${id}.${attr.key}`)
|
||||
}
|
||||
}
|
||||
for (const { id, absent } of plan) {
|
||||
for (const attr of absent) await api.waitForAttribute(id, attr.key)
|
||||
}
|
||||
log('Fertig.')
|
||||
return result
|
||||
}
|
||||
|
||||
const invokedDirectly = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href
|
||||
if (invokedDirectly) {
|
||||
const pruefen = process.argv.includes('--pruefen') || process.argv.includes('--check')
|
||||
main({ pruefen })
|
||||
.then(({ missing }) => process.exit(pruefen && missing.length ? 1 : 0))
|
||||
.catch((e) => {
|
||||
console.error(e.message)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
@@ -188,8 +188,8 @@ test('Schema featureConsents: drei Indizes mit den Attributen aus der Vorgabe (i
|
||||
})
|
||||
|
||||
test('schemaKeys(featureConsents) liefert dieselben Schluessel, der Nachbau nimmt sie an und lehnt andere ab', async () => {
|
||||
assert.deepEqual(schemaKeys('featureConsents'), CONSENT_KEYS)
|
||||
assert.deepEqual(schemaKeys('featureConsents'), FEATURE_CONSENT_ATTRIBUTES.map((a) => a.key))
|
||||
assert.deepEqual(schemaKeys('featureConsents'), [...CONSENT_KEYS, 'kundenart'])
|
||||
assert.deepEqual(schemaKeys('featureConsents'), [...FEATURE_CONSENT_ATTRIBUTES.map((a) => a.key), 'kundenart'])
|
||||
schema.featureConsents = schemaKeys('featureConsents')
|
||||
const { createDocument } = await import('../server/services/appwriteAdmin.js')
|
||||
const doc = await createDocument(config.collections.featureConsents, {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { test, after } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { startAppwriteMock } from './support/appwriteMock.js'
|
||||
import { useTestEnv } from './support/testEnv.js'
|
||||
import { schemaKeys } from './support/schema.js'
|
||||
import { KUNDENANSICHT_ATTRIBUTE, main } from '../scripts/kundenansicht-setup.mjs'
|
||||
|
||||
// Setup-Skript der Kundenansicht neu (Paket P5): Kundenart im Nachweis, Einrichtungs-Marker an Ticket und Funktion.
|
||||
// Das Skript laeuft hier nur gegen Mock-Clients, nie gegen Produktion.
|
||||
const aw = await startAppwriteMock({ schema: {} })
|
||||
after(() => aw.close())
|
||||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: 'http://127.0.0.1:1' })
|
||||
const { config } = await import('../server/config.js')
|
||||
|
||||
const client = (vorhanden = {}) => {
|
||||
const calls = { attrs: [], waits: [] }
|
||||
return {
|
||||
calls,
|
||||
collectionExists: async () => true,
|
||||
listAttributes: async (id) => (vorhanden[id] ?? []).map((key) => ({ key })),
|
||||
createAttribute: async (id, a) => calls.attrs.push(`${id}.${a.key}`),
|
||||
waitForAttribute: async (id, k) => calls.waits.push(`${id}.${k}`),
|
||||
}
|
||||
}
|
||||
const ID = () => config.collections
|
||||
const ALLE = () => [
|
||||
`${ID().featureConsents}.kundenart`,
|
||||
`${ID().portalFeatures}.einrichtungAbrechnung`,
|
||||
`${ID().portalFeatures}.unlockedBy`,
|
||||
`${ID().workorders}.einrichtungAbrechnung`,
|
||||
`${ID().workorders}.einrichtungFuer`,
|
||||
]
|
||||
|
||||
test('Schema: Schlüssel, Größen, nur optionale Strings; längste Werte passen hinein', () => {
|
||||
const kurz = Object.fromEntries(Object.entries(KUNDENANSICHT_ATTRIBUTE).map(([c, l]) => [c, l.map((a) => `${a.key}:${a.size}`)]))
|
||||
assert.deepEqual(kurz, {
|
||||
featureConsents: ['kundenart:12'],
|
||||
workorders: ['einrichtungFuer:16', 'einrichtungAbrechnung:8'],
|
||||
portalFeatures: ['unlockedBy:8', 'einrichtungAbrechnung:8'],
|
||||
})
|
||||
assert.ok(Object.values(KUNDENANSICHT_ATTRIBUTE).flat().every((a) => a.type === 'string' && a.required === false))
|
||||
assert.ok('unternehmen'.length <= 12 && 'projekt'.length <= 8 && 'unterkunft'.length <= 16)
|
||||
})
|
||||
|
||||
test('--pruefen meldet alle fünf Lücken und legt nichts an', async () => {
|
||||
const c = client()
|
||||
const log = []
|
||||
const r = await main({ pruefen: true, client: c, log: (z) => log.push(z) })
|
||||
assert.deepEqual(r.missing.map((a) => `${a.collection}.${a.key}`).sort(), ALLE())
|
||||
assert.deepEqual(c.calls, { attrs: [], waits: [] })
|
||||
assert.ok(log.some((z) => z === `- ${ID().featureConsents}.kundenart FEHLT`))
|
||||
})
|
||||
|
||||
test('ohne --pruefen: legt an und wartet; der zweite Lauf ist idempotent', async () => {
|
||||
const c = client()
|
||||
await main({ client: c, log: () => {} })
|
||||
assert.deepEqual([...c.calls.attrs].sort(), ALLE())
|
||||
assert.deepEqual([...c.calls.waits].sort(), ALLE())
|
||||
const voll = client({
|
||||
[ID().featureConsents]: ['kundenart'],
|
||||
[ID().workorders]: ['einrichtungFuer', 'einrichtungAbrechnung'],
|
||||
[ID().portalFeatures]: ['unlockedBy', 'einrichtungAbrechnung'],
|
||||
})
|
||||
assert.deepEqual((await main({ client: voll, log: () => {} })).missing, [])
|
||||
assert.deepEqual(voll.calls.attrs, [])
|
||||
})
|
||||
|
||||
test('schemaKeys liest das Skript; das Skript löscht nichts', () => {
|
||||
assert.equal(schemaKeys('featureConsents').at(-1), 'kundenart')
|
||||
const f = schemaKeys('portalFeatures')
|
||||
assert.ok(f.includes('einrichtungAbrechnung'))
|
||||
assert.equal(new Set(f).size, f.length)
|
||||
assert.doesNotMatch(readFileSync(new URL('../scripts/kundenansicht-setup.mjs', import.meta.url), 'utf8'), /delete/i)
|
||||
})
|
||||
@@ -5,6 +5,7 @@ import { UNTERKUNFT_COLLECTIONS } from '../../server/services/unterkunft/schema.
|
||||
import { ZEITEN_COLLECTIONS, ZEITEN_FEATURE_ATTRIBUTES } from '../../server/services/zeiten/schema.js'
|
||||
import { AUFTRAEGE_COLLECTIONS, AUFTRAEGE_ZUSATZ_ATTRIBUTE } from '../../server/services/auftraege/schema.js'
|
||||
import { FEATURE_CONSENT_ATTRIBUTES, PORTAL_FEATURE_ATTRIBUTES } from '../../scripts/feature-setup.mjs'
|
||||
import { KUNDENANSICHT_ATTRIBUTE } from '../../scripts/kundenansicht-setup.mjs'
|
||||
|
||||
// Setup-Skripte, die Collections in Appwrite anlegen (Blockformat: 'id', ... \n ],)
|
||||
const SETUP_SCRIPTS = ['setup-abo-collections.mjs', 'setup-finanzen.mjs']
|
||||
@@ -20,6 +21,8 @@ const SETUP_SCRIPTS = ['setup-abo-collections.mjs', 'setup-finanzen.mjs']
|
||||
* portalFeatures bekommt zusaetzlich die Attribute aus scripts/feature-setup.mjs (PORTAL_FEATURE_ATTRIBUTES) und die
|
||||
* Zeiten-Felder aus server/services/zeiten/schema.js (ZEITEN_FEATURE_ATTRIBUTES, angelegt von scripts/zeiten-setup.mjs);
|
||||
* featureConsents (Zustimmungsnachweis) steht ganz dort (FEATURE_CONSENT_ATTRIBUTES);
|
||||
* scripts/kundenansicht-setup.mjs (KUNDENANSICHT_ATTRIBUTE) ergaenzt featureConsents um kundenart und portalFeatures um
|
||||
* unlockedBy und einrichtungAbrechnung;
|
||||
* die Aufträge-Collections kommen aus server/services/auftraege/schema.js (scripts/auftraege-setup.mjs), und ihre
|
||||
* Zusatzfelder bestehender Collections (AUFTRAEGE_ZUSATZ_ATTRIBUTE, z. B. financeSettings.steuernummer) werden angehängt.
|
||||
*/
|
||||
@@ -29,7 +32,7 @@ export function schemaKeys(collection) {
|
||||
if (Object.hasOwn(SHOP_COLLECTIONS, collection)) return SHOP_COLLECTIONS[collection].attributes.map((a) => a.key)
|
||||
if (Object.hasOwn(UNTERKUNFT_COLLECTIONS, collection)) return UNTERKUNFT_COLLECTIONS[collection].attributes.map((a) => a.key)
|
||||
if (Object.hasOwn(ZEITEN_COLLECTIONS, collection)) return ZEITEN_COLLECTIONS[collection].attributes.map((a) => a.key)
|
||||
if (collection === 'featureConsents') return FEATURE_CONSENT_ATTRIBUTES.map((a) => a.key)
|
||||
if (collection === 'featureConsents') return [...FEATURE_CONSENT_ATTRIBUTES, ...KUNDENANSICHT_ATTRIBUTE.featureConsents].map((a) => a.key)
|
||||
for (const script of SETUP_SCRIPTS) {
|
||||
const src = readFileSync(new URL(`../../scripts/${script}`, import.meta.url), 'utf8')
|
||||
const start = src.indexOf(`'${collection}',`)
|
||||
@@ -37,7 +40,8 @@ export function schemaKeys(collection) {
|
||||
const block = src.slice(start, src.indexOf('\n ],', start))
|
||||
const keys = [...block.matchAll(/key: '(\w+)'/g)].map((m) => m[1])
|
||||
if (collection === 'portalFeatures') {
|
||||
return [...keys, ...PORTAL_FEATURE_ATTRIBUTES.map((a) => a.key), ...ZEITEN_FEATURE_ATTRIBUTES.map((a) => a.key)]
|
||||
const zusatz = [...PORTAL_FEATURE_ATTRIBUTES, ...ZEITEN_FEATURE_ATTRIBUTES, ...KUNDENANSICHT_ATTRIBUTE.portalFeatures]
|
||||
return [...new Set([...keys, ...zusatz.map((a) => a.key)])]
|
||||
}
|
||||
return Object.hasOwn(AUFTRAEGE_ZUSATZ_ATTRIBUTE, collection)
|
||||
? [...keys, ...AUFTRAEGE_ZUSATZ_ATTRIBUTE[collection].map((a) => a.key)]
|
||||
|
||||
Reference in new issue
Block a user