membership: refactor form filling, add smoke tests and debug-guard fallback; fix mappings

This commit is contained in:
Torsten Schulz (local)
2025-10-23 14:21:05 +02:00
parent f14597006e
commit e029154a8c
9 changed files with 662 additions and 295 deletions

View File

@@ -507,11 +507,19 @@ Das Vereinsmitglied trifft die Entscheidung zur Veröffentlichung seiner Daten i
const sigLabelSize = 10
page3.drawText(sigLabel, { x: dateX, y: dateFieldY - 18, size: sigLabelSize, font: helv })
// Ensure appearance streams are generated for all form fields using the embedded font
try {
form.updateFieldAppearances(helv)
} catch (e) {
console.warn('Warning: updateFieldAppearances failed while generating template:', e)
}
const pdfBytes = await pdfDoc.save()
fs.writeFileSync('server/templates/mitgliedschaft-fillable.pdf', pdfBytes)
console.log('Wrote server/templates/mitgliedschaft-fillable.pdf')
}
create().catch(e => {
console.error(e)
process.exit(1)

View File

@@ -12,6 +12,9 @@ async function fill() {
const pdfDoc = await PDFDocument.load(existingPdfBytes)
const form = pdfDoc.getForm()
// Ensure a readable font is embedded and used for field appearances
const helv = await pdfDoc.embedFont(StandardFonts.Helvetica)
// Simple sample data
const sample = {
nachname: 'Müller',
@@ -51,45 +54,200 @@ async function fill() {
}
}
safeSetText('nachname', sample.nachname)
safeSetText('vorname', sample.vorname)
safeSetText('strasse', sample.strasse)
safeSetText('plz_ort', sample.plz_ort)
safeSetText('geburtsdatum', sample.geburtsdatum)
safeSetText('telefon', sample.telefon)
safeSetText('email', sample.email)
safeSetText('telefon_mobil', sample.telefon_mobil)
// Robust setter: find a field by name (case-insensitive) and set text/checkbox/select accordingly
function setFieldByName(name, value) {
try {
const lower = name.toLowerCase()
const field = form.getFields().find(f => f.getName() && f.getName().toLowerCase() === lower)
if (!field) {
console.log(`DEBUG: Field not found for '${name}'`)
return false
}
// Text field
if (typeof field.setText === 'function') {
field.setText(value == null ? '' : String(value))
return true
}
// Check box
if (typeof field.check === 'function') {
if (value === true || String(value).toLowerCase() === 'true') field.check()
else if (typeof field.uncheck === 'function') field.uncheck()
return true
}
// Radio/select (pdf-lib uses select for dropdowns)
if (typeof field.select === 'function') {
try { field.select(String(value)) } catch (e) { /* ignore */ }
return true
}
console.log(`DEBUG: Unsupported field type for '${name}'`)
return false
} catch (e) {
console.log(`DEBUG: Error setting field '${name}':`, e.message)
return false
}
}
// Debug: list all form fields found in the template
try {
const cbA = form.getCheckBox('mitglied_aktiv')
if (sample.mitglied_aktiv) cbA.check(); else cbA.uncheck()
} catch(e) {}
try {
const cbP = form.getCheckBox('mitglied_passiv')
if (sample.mitglied_passiv) cbP.check(); else cbP.uncheck()
} catch(e) {}
const allFields = form.getFields().map(f => f.getName())
console.log('DEBUG: Template form fields:', allFields.join(', '))
} catch (e) {
console.log('DEBUG: Could not list form fields:', e.message)
}
safeSetText('sepa_mitglied', sample.sepa_mitglied)
safeSetText('sepa_kontoinhaber', sample.sepa_kontoinhaber)
safeSetText('sepa_strasse', sample.sepa_strasse)
safeSetText('sepa_plz_ort', sample.sepa_plz_ort)
safeSetText('sepa_bank', sample.sepa_bank)
safeSetText('sepa_iban', sample.sepa_iban)
safeSetText('sepa_bic', sample.sepa_bic)
safeSetText('sepa_datum', sample.sepa_datum)
safeSetText('sign_datum', sample.sign_datum)
setFieldByName('nachname', sample.nachname)
setFieldByName('vorname', sample.vorname)
setFieldByName('strasse', sample.strasse)
setFieldByName('plz_ort', sample.plz_ort)
setFieldByName('geburtsdatum', sample.geburtsdatum)
setFieldByName('telefon', sample.telefon)
setFieldByName('email', sample.email)
setFieldByName('telefon_mobil', sample.telefon_mobil)
// Checkboxes via robust setter
setFieldByName('mitglied_aktiv', sample.mitglied_aktiv)
setFieldByName('mitglied_passiv', sample.mitglied_passiv)
setFieldByName('sepa_mitglied', sample.sepa_mitglied)
setFieldByName('sepa_kontoinhaber', sample.sepa_kontoinhaber)
setFieldByName('sepa_strasse', sample.sepa_strasse)
setFieldByName('sepa_plz_ort', sample.sepa_plz_ort)
setFieldByName('sepa_bank', sample.sepa_bank)
setFieldByName('sepa_iban', sample.sepa_iban)
setFieldByName('sepa_bic', sample.sepa_bic)
setFieldByName('sepa_datum', sample.sepa_datum)
setFieldByName('sign_datum', sample.sign_datum)
// page3 fields
safeSetText('page3_name', sample.page3_name)
safeSetText('page3_vorname', sample.page3_vorname)
safeSetText('page3_anschrift', sample.page3_anschrift)
safeSetText('page3_telefon', sample.page3_telefon)
safeSetText('page3_fax', sample.page3_fax)
safeSetText('page3_email', sample.page3_email)
safeSetText('page3_datum', sample.page3_datum)
setFieldByName('page3_name', sample.page3_name)
setFieldByName('page3_vorname', sample.page3_vorname)
setFieldByName('page3_anschrift', sample.page3_anschrift)
setFieldByName('page3_telefon', sample.page3_telefon)
setFieldByName('page3_fax', sample.page3_fax)
setFieldByName('page3_email', sample.page3_email)
setFieldByName('page3_datum', sample.page3_datum)
// flatten all fields
try { form.flatten() } catch (e) {}
// Debug: check which sample keys correspond to actual fields
try {
const names = form.getFields().map(f => f.getName().toLowerCase())
for (const key of Object.keys(sample)) {
const found = names.includes(key.toLowerCase())
console.log(`DEBUG: sample key='${key}' -> field present=${found}`)
}
} catch (e) {
console.log('DEBUG: field presence check failed:', e.message)
}
// Debug: read back all field values after setting (before flattening)
try {
console.log('DEBUG: Field values after setting:')
for (const f of form.getFields()) {
const name = f.getName()
let val = null
try {
if (typeof f.getText === 'function') val = f.getText()
else if (typeof f.isChecked === 'function') val = f.isChecked()
else val = '(no getter)'
} catch (e) {
val = `(error reading: ${e.message})`
}
console.log(` ${name}: ${val}`)
}
} catch (e) {
console.log('DEBUG: Could not read back field values:', e.message)
}
// Debug: print widget rectangles for relevant fields (SEPA and page3)
try {
const interesting = ['sepa_mitglied','sepa_kontoinhaber','sepa_strasse','sepa_plz_ort','sepa_bank','sepa_iban','sepa_bic','page3_name','page3_vorname','page3_anschrift','page3_telefon','page3_email']
for (const fname of interesting) {
const f = form.getFields().find(x => x.getName && x.getName().toLowerCase() === fname)
if (!f) { console.log(`DEBUG: no field object for ${fname}`); continue }
try {
// attempt to access widget rectangle via low-level acroField
const acro = f.acroField
const widgets = acro.getWidgets()
if (!widgets || widgets.length === 0) { console.log(`DEBUG: no widgets for ${fname}`); continue }
const rect = widgets[0].getRectangle()
console.log(`DEBUG: widget rect for ${fname}: ${JSON.stringify(rect)}`)
} catch (e) {
console.log(`DEBUG: cannot read widget rect for ${fname}: ${e.message}`)
}
}
} catch (e) {
console.log('DEBUG: widget rect inspection failed:', e.message)
}
// Define fallback drawing: draw visible text directly onto pages at widget rect positions
async function fallbackDraw() {
try {
const pages = pdfDoc.getPages()
// draw SEPA fields on page 2 (index 1)
const p2 = pages[1]
const sepaFields = ['sepa_mitglied','sepa_kontoinhaber','sepa_strasse','sepa_plz_ort','sepa_bank','sepa_iban','sepa_bic']
for (const fname of sepaFields) {
const f = form.getFields().find(x => x.getName && x.getName().toLowerCase() === fname)
if (!f) continue
try {
const widgets = f.acroField.getWidgets()
if (!widgets || widgets.length === 0) continue
const rect = widgets[0].getRectangle()
const text = (typeof f.getText === 'function') ? f.getText() : ''
if (text) {
p2.drawText(String(text), { x: rect.x + 2, y: rect.y + rect.height - 12, size: 11, font: helv })
console.log(`FALLBACK: drew ${fname} on page2 at ${rect.x},${rect.y}`)
}
} catch (e) {
console.log(`FALLBACK: could not draw ${fname}: ${e.message}`)
}
}
// draw page3 fields on page 3 (index 2)
const p3 = pages[2]
const p3Fields = ['page3_name','page3_vorname','page3_anschrift','page3_telefon','page3_email']
for (const fname of p3Fields) {
const f = form.getFields().find(x => x.getName && x.getName().toLowerCase() === fname)
if (!f) continue
try {
const widgets = f.acroField.getWidgets()
if (!widgets || widgets.length === 0) continue
const rect = widgets[0].getRectangle()
const text = (typeof f.getText === 'function') ? f.getText() : ''
if (text) {
p3.drawText(String(text), { x: rect.x + 2, y: rect.y + rect.height - 12, size: 11, font: helv })
console.log(`FALLBACK: drew ${fname} on page3 at ${rect.x},${rect.y}`)
}
} catch (e) {
console.log(`FALLBACK: could not draw ${fname}: ${e.message}`)
}
}
// write fallback copy
const fallbackBytes = await pdfDoc.save()
if (!fs.existsSync('temp')) fs.mkdirSync('temp')
fs.writeFileSync('temp/mitgliedschaft-sample-filled-fallback.pdf', fallbackBytes)
console.log('Wrote temp/mitgliedschaft-sample-filled-fallback.pdf')
} catch (e) {
console.log('FALLBACK drawing failed:', e.message)
}
}
// Update field appearances so text is visible, then flatten. Run fallback only when enabled.
try {
form.updateFieldAppearances(helv)
const outUnflattened = await pdfDoc.save()
if (!fs.existsSync('temp')) fs.mkdirSync('temp')
fs.writeFileSync('temp/mitgliedschaft-sample-filled-unflattened.pdf', outUnflattened)
form.flatten()
} catch (e) {
console.warn('Could not update field appearances:', e.message)
const enableFallback = process.env.ENABLE_FALLBACK === '1' || (typeof sample.debug !== 'undefined' && sample.debug)
if (enableFallback) {
try { await fallbackDraw() } catch (err) { console.warn('Fallback draw failed:', err.message) }
try { form.flatten() } catch (err) { /* ignore */ }
}
}
const out = await pdfDoc.save()
if (!fs.existsSync('temp')) fs.mkdirSync('temp')

View File

@@ -0,0 +1,40 @@
import fs from 'fs'
import path from 'path'
import { PDFDocument } from 'pdf-lib'
async function main() {
const uploads = path.join(process.cwd(), 'public', 'uploads')
const files = fs.existsSync(uploads) ? fs.readdirSync(uploads).filter(f => f.toLowerCase().endsWith('.pdf')) : []
if (files.length === 0) { console.log('no pdfs'); return }
files.sort((a,b) => fs.statSync(path.join(uploads,b)).mtimeMs - fs.statSync(path.join(uploads,a)).mtimeMs)
const latest = path.join(uploads, files[0])
console.log('Inspecting', latest)
const bytes = fs.readFileSync(latest)
const pdf = await PDFDocument.load(bytes)
let form
try { form = pdf.getForm() } catch (e) { form = null }
if (!form) { console.log('no form'); return }
const fields = form.getFields()
const matches = []
for (const f of fields) {
const name = f.getName()
try {
if (typeof f.getText === 'function') {
const v = f.getText()
if (v && (String(v).toLowerCase() === 'aktiv' || String(v).toLowerCase() === 'passiv')) {
matches.push({ name, value: v })
}
} else if (typeof f.isChecked === 'function') {
const checked = f.isChecked()
if (checked) {
// value true -> possibly membership
matches.push({ name, value: 'checked' })
}
}
} catch (e) {}
}
if (matches.length === 0) console.log('no aktiv/passiv values found')
else console.log('matches:', matches)
}
main().catch(e => { console.error(e); process.exit(1) })

77
scripts/inspect-forms.js Normal file
View File

@@ -0,0 +1,77 @@
import fs from 'fs'
import path from 'path'
import { PDFDocument, StandardFonts } from 'pdf-lib'
async function inspect(pdfPath) {
console.log('\n--- Inspecting', pdfPath)
if (!fs.existsSync(pdfPath)) {
console.log('MISSING:', pdfPath)
return
}
const bytes = fs.readFileSync(pdfPath)
const pdfDoc = await PDFDocument.load(bytes)
let form = null
try { form = pdfDoc.getForm() } catch (e) { form = null }
if (!form) { console.log('No AcroForm found') ; return }
const fields = form.getFields()
console.log('Field count:', fields.length)
for (const f of fields) {
const name = f.getName()
let value = null
try {
if (typeof f.getText === 'function') value = f.getText()
else if (typeof f.isChecked === 'function') value = f.isChecked()
else value = '(no getter)'
} catch (e) {
value = `(error reading: ${e.message})`
}
// widgets
let widgetsInfo = []
try {
const acro = f.acroField
const widgets = acro.getWidgets()
for (const w of widgets) {
try {
const rect = w.getRectangle()
// try to find page index by searching pages for an annotation with same ref
let pageIndex = null
try {
const pages = pdfDoc.getPages()
for (let i = 0; i < pages.length; i++) {
const page = pages[i]
const annots = page.node.Annots ? page.node.Annots() : null
// can't reliably map here; just record rect
}
} catch (e) {}
widgetsInfo.push({ rect })
} catch (e) {
widgetsInfo.push({ error: e.message })
}
}
} catch (e) {
widgetsInfo = [`error widgets: ${e.message}`]
}
console.log(`- ${name}: value='${value}' widgets=${widgetsInfo.length}`)
for (const wi of widgetsInfo) console.log(' ', JSON.stringify(wi))
}
}
async function main() {
const repoRoot = process.cwd()
const template = path.join(repoRoot, 'server', 'templates', 'mitgliedschaft-fillable.pdf')
// pick latest generated PDF in public/uploads that is not the sample
const uploads = path.join(repoRoot, 'public', 'uploads')
let pdfFiles = []
if (fs.existsSync(uploads)) {
pdfFiles = fs.readdirSync(uploads).filter(f => f.toLowerCase().endsWith('.pdf'))
.map(f => ({ f, mtime: fs.statSync(path.join(uploads, f)).mtimeMs }))
.sort((a,b) => b.mtime - a.mtime)
.map(x => x.f)
}
const apiPdf = pdfFiles.find(n => !n.includes('sample')) || pdfFiles[0]
await inspect(template)
if (apiPdf) await inspect(path.join(uploads, apiPdf))
else console.log('No API-generated PDF found in public/uploads')
}
main().catch(e => { console.error(e); process.exit(1) })

25
scripts/smoke-test.js Normal file
View File

@@ -0,0 +1,25 @@
import { execSync } from 'child_process'
import fs from 'fs'
import path from 'path'
function run(cmd) {
console.log('> ', cmd)
try { const out = execSync(cmd, { stdio: 'pipe' }).toString(); console.log(out); return out } catch (e) { console.error('ERROR:', e.message); return null }
}
async function main() {
const root = process.cwd()
run('node scripts/create-fillable-template.js')
run('node scripts/fill-sample-template.js')
const uploads = path.join(root, 'public', 'uploads')
const files = fs.existsSync(uploads) ? fs.readdirSync(uploads).filter(f => f.toLowerCase().endsWith('.pdf')) : []
console.log('Uploads PDFs:', files)
// try API if server env present
const apiUrl = process.env.MEMBERSHIP_API_URL || ''
if (apiUrl) {
run(`curl -sS -X POST "${apiUrl}" -H 'Content-Type: application/json' -d '{"nachname":"Test","vorname":"Smoke","strasse":"X","plz":"00000","ort":"Local","geburtsdatum":"1990-01-01","telefon_privat":"000","telefon_mobil":"000","email":"x@example.com","mitgliedschaftsart":"aktiv","kontoinhaber":"Smoke Test","iban":"DE00","bic":"XXXX","bank":"Local"}' -D - | sed -n '1,80p'`)
}
run('node scripts/inspect-forms.js')
}
main()