refactor(clickTtPlayerRegistrationService, memberController): improve error handling and diagnostics

- Updated error response structure in memberController to include detailed information instead of a trace array, enhancing clarity for the client.
- Enhanced ClickTtPlayerRegistrationService to capture and return detailed information about the selected search result and last submission attempt, improving error diagnostics.
- Modified frontend to format and display the new detailed error information, providing better context for users during registration failures.
This commit is contained in:
Torsten Schulz (local)
2026-03-11 17:25:15 +01:00
parent 312a1d9d8a
commit 7cb6b66971
3 changed files with 107 additions and 30 deletions

View File

@@ -102,6 +102,8 @@ class ClickTtPlayerRegistrationService {
let context = null;
let page = null;
const trace = [];
let selectedSearchResult = null;
let lastSubmitResult = null;
try {
browser = await chromium.launch({
@@ -121,15 +123,18 @@ class ClickTtPlayerRegistrationService {
await this._clickByText(page, 'Personen suchen', trace);
await page.waitForLoadState('domcontentloaded');
await this._openApplicationAfterSearch(page, memberJson, trace);
selectedSearchResult = await this._openApplicationAfterSearch(page, memberJson, trace);
await this._fillApplicationForm(page, memberJson, primaryEmail, trace);
await this._clickByText(page, 'Weiter >>', trace);
await page.waitForLoadState('domcontentloaded');
lastSubmitResult = await this._buildLastSubmitResult(page, 'Weiter >>');
await this._dismissConsentOverlays(page, trace);
await this._clickByText(page, 'Speichern', trace);
await page.waitForLoadState('domcontentloaded');
lastSubmitResult = await this._buildLastSubmitResult(page, 'Speichern');
await this._clickByText(page, 'Einreichen', trace);
await page.waitForLoadState('domcontentloaded');
lastSubmitResult = await this._buildLastSubmitResult(page, 'Einreichen');
const finalText = sanitizePageText(await page.locator('body').innerText());
const storageState = await context.storageState();
@@ -145,14 +150,19 @@ class ClickTtPlayerRegistrationService {
message: `Spielberechtigung fuer ${memberJson.firstName} ${memberJson.lastName} wurde in click-TT eingereicht.`,
finalUrl: page.url(),
finalText,
trace
details: {
selectedSearchResult,
lastSubmitResult
}
};
} catch (error) {
const diagnostics = {
url: null,
text: null,
traceTail: trace.slice(-25),
htmlPath: null
htmlPath: null,
selectedSearchResult,
lastSubmitResult
};
try {
@@ -191,6 +201,10 @@ class ClickTtPlayerRegistrationService {
const wrappedError = new HttpError(`${message}${diagnostics.url ? ` (Seite: ${diagnostics.url})` : ''}${diagnostics.htmlPath ? ` (HTML: ${diagnostics.htmlPath})` : ''}${diagnostics.text ? ` - ${diagnostics.text}` : ''}`, error.statusCode || error.status || 500);
wrappedError.trace = diagnostics.traceTail || [];
wrappedError.htmlPath = diagnostics.htmlPath || null;
wrappedError.details = {
selectedSearchResult: diagnostics.selectedSearchResult || null,
lastSubmitResult: diagnostics.lastSubmitResult || null
};
throw wrappedError;
} finally {
if (context) {
@@ -484,7 +498,7 @@ class ClickTtPlayerRegistrationService {
const expectedName = normalizeComparableText(`${member.lastName}, ${member.firstName}`);
const expectedBirthDate = formatGermanDate(member.birthDate);
const explicitApplicationHref = await page.locator('table.result-set tr').evaluateAll((rows, criteria) => {
const selectedResult = await page.locator('table.result-set tr').evaluateAll((rows, criteria) => {
const normalize = (value) => String(value || '')
.normalize('NFKC')
.replace(/\s+/g, ' ')
@@ -504,7 +518,12 @@ class ClickTtPlayerRegistrationService {
if (nameText !== criteria.expectedName) continue;
if (birthDateText !== normalize(criteria.expectedBirthDate)) continue;
return link.getAttribute('href');
return {
href: link.getAttribute('href'),
name: String(strong.textContent || '').replace(/\s+/g, ' ').trim(),
birthDate: String(cells[1]?.textContent || '').replace(/\s+/g, ' ').trim(),
section: String(row.closest('table')?.querySelector('h2')?.textContent || '').replace(/\s+/g, ' ').trim()
};
}
return null;
@@ -513,8 +532,8 @@ class ClickTtPlayerRegistrationService {
expectedBirthDate
});
if (explicitApplicationHref) {
const explicitApplicationLink = page.locator(`a[href="${explicitApplicationHref}"]`).first();
if (selectedResult?.href) {
const explicitApplicationLink = page.locator(`a[href="${selectedResult.href}"]`).first();
let dialogSeen = false;
page.once('dialog', async (dialog) => {
dialogSeen = true;
@@ -528,9 +547,11 @@ class ClickTtPlayerRegistrationService {
this._trace(trace, 'step', {
name: 'click',
label: 'Spielberechtigung beantragen',
selector: `a[href="${explicitApplicationHref}"]`,
selector: `a[href="${selectedResult.href}"]`,
expectedName: `${member.lastName}, ${member.firstName}`,
expectedBirthDate
expectedBirthDate,
selectedName: selectedResult.name,
selectedBirthDate: selectedResult.birthDate
});
await explicitApplicationLink.click();
await page.waitForLoadState('domcontentloaded');
@@ -540,7 +561,7 @@ class ClickTtPlayerRegistrationService {
dialogSeen,
url: page.url()
});
return;
return selectedResult;
}
const searchResultsText = sanitizePageText(await page.locator('table.result-set').innerText().catch(() => ''));
@@ -552,6 +573,48 @@ class ClickTtPlayerRegistrationService {
}
}
async _buildLastSubmitResult(page, action) {
return {
action,
url: page.url(),
applicant: await this._readCurrentApplicant(page),
pageText: sanitizePageText(await page.locator('body').innerText().catch(() => ''))
};
}
async _readCurrentApplicant(page) {
return page.evaluate(() => {
const form = document.querySelector('form.edit-object');
if (!form) {
return null;
}
const readValue = (selector) => {
const field = form.querySelector(selector);
if (!field) return '';
if (field.tagName === 'SELECT') {
const selected = field.options?.[field.selectedIndex];
return String(selected?.textContent || '').replace(/\s+/g, ' ').trim();
}
return String(field.value || '').replace(/\s+/g, ' ').trim();
};
const lastName = readValue('input[name$=".15"], input[name$=".31.1.1.5.3"]');
const firstName = readValue('input[name$=".17"], input[name$=".31.1.1.5.5"]');
const birthDate = readValue('input[name$=".25"], input[name$=".31.1.1.5.11"]');
if (!lastName && !firstName && !birthDate) {
return null;
}
return {
lastName,
firstName,
birthDate
};
}).catch(() => null);
}
async _fillFirstAvailable(page, selectors, value) {
for (const selector of selectors) {
const locator = page.locator(selector).first();