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

@@ -225,7 +225,7 @@ const requestClickTtPlayerRegistration = async (req, res) => {
res.status(error.statusCode || error.status || 500).json({
success: false,
error: error.message || 'Click-TT-Antrag konnte nicht eingereicht werden',
trace: Array.isArray(error.trace) ? error.trace : []
details: error.details || null
});
}
};

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();

View File

@@ -675,10 +675,10 @@ export default {
this.clickTtPendingMemberIds = [...this.clickTtPendingMemberIds, member.id];
try {
const response = await apiClient.post(`/clubmembers/clicktt-registration/${this.currentClub}/${member.id}`);
const traceDetails = this.formatClickTtTrace(response.data?.trace);
const detailsText = this.formatClickTtDetails(response.data?.details);
if (response.data?.success) {
member.clickTtApplicationSubmitted = true;
const successDetails = [response.data.finalUrl || '', traceDetails].filter(Boolean).join('\n\n');
const successDetails = [response.data.finalUrl || '', detailsText].filter(Boolean).join('\n\n');
await this.showInfo(
'Click-TT-Antrag',
getSafeMessage(response.data.message, 'Der Click-TT-Antrag wurde erfolgreich eingereicht.'),
@@ -689,7 +689,7 @@ export default {
await this.showInfo(
'Click-TT-Antrag',
getSafeMessage(response.data?.error, 'Der Click-TT-Antrag konnte nicht eingereicht werden.'),
traceDetails,
detailsText,
'error'
);
}
@@ -699,33 +699,47 @@ export default {
await this.showInfo(
'Click-TT-Antrag',
errorMessage,
this.formatClickTtTrace(error?.response?.data?.trace),
this.formatClickTtDetails(error?.response?.data?.details),
'error'
);
} finally {
this.clickTtPendingMemberIds = this.clickTtPendingMemberIds.filter(id => id !== member.id);
}
},
formatClickTtTrace(trace) {
if (!Array.isArray(trace) || trace.length === 0) {
formatClickTtDetails(details) {
if (!details) {
return '';
}
return trace
.slice(-12)
.map((entry) => {
const type = entry?.type || 'trace';
const parts = [type];
const parts = [];
const selected = details.selectedSearchResult;
const lastSubmit = details.lastSubmitResult;
if (entry?.name) parts.push(entry.name);
if (entry?.label) parts.push(`label=${entry.label}`);
if (entry?.status) parts.push(`status=${entry.status}`);
if (entry?.url) parts.push(`url=${entry.url}`);
if (entry?.message) parts.push(`message=${entry.message}`);
if (selected?.name || selected?.birthDate || selected?.href) {
parts.push([
'Ausgewaehlter Suchtreffer:',
selected?.name ? `Name: ${selected.name}` : '',
selected?.birthDate ? `Geburtsdatum: ${selected.birthDate}` : '',
selected?.href ? `Link: ${selected.href}` : ''
].filter(Boolean).join('\n'));
}
return parts.join(' | ');
})
.join('\n');
if (lastSubmit?.action || lastSubmit?.applicant || lastSubmit?.pageText) {
parts.push([
'Ergebnis letzter Submit:',
lastSubmit?.action ? `Aktion: ${lastSubmit.action}` : '',
lastSubmit?.applicant
? `Antrag fuer: ${[
lastSubmit.applicant.lastName,
lastSubmit.applicant.firstName
].filter(Boolean).join(', ')}${lastSubmit.applicant.birthDate ? ` (${lastSubmit.applicant.birthDate})` : ''}`
: '',
lastSubmit?.url ? `URL: ${lastSubmit.url}` : '',
lastSubmit?.pageText ? `Seitenstatus: ${lastSubmit.pageText}` : ''
].filter(Boolean).join('\n'));
}
return parts.join('\n\n');
},
toggleNewMember() {
this.memberFormIsOpen = !this.memberFormIsOpen;