feat(clickTtPlayerRegistrationService): enhance registration flow with club context and error handling

- Integrated Club model to retrieve association member number during player registration, ensuring necessary data is available.
- Added methods to select club context and ensure application entry visibility, improving navigation within the Click-TT interface.
- Enhanced error handling for missing association member numbers, providing clearer feedback to users during the registration process.
- Updated consent overlay handling to streamline user interactions and improve automation in the registration flow.
This commit is contained in:
Torsten Schulz (local)
2026-03-11 15:55:21 +01:00
parent 7196fae28e
commit 2919ee3764

View File

@@ -1,5 +1,6 @@
import { chromium } from 'playwright';
import Member from '../models/Member.js';
import Club from '../models/Club.js';
import ClickTtAccount from '../models/ClickTtAccount.js';
import { checkAccess } from '../utils/userUtils.js';
import HttpError from '../exceptions/HttpError.js';
@@ -46,6 +47,12 @@ class ClickTtPlayerRegistrationService {
throw new HttpError('Mitglied nicht gefunden', 404);
}
const club = await Club.findByPk(clubId);
const associationMemberNumber = String(club?.associationMemberNumber || '').trim();
if (!associationMemberNumber) {
throw new HttpError('Für den Verein ist keine Verbands-Mitgliedsnummer hinterlegt', 400);
}
const account = await ClickTtAccount.findOne({ where: { userId } });
if (!account?.username) {
throw new HttpError('Kein HTTV-/click-TT-Account für diesen Benutzer hinterlegt', 400);
@@ -82,6 +89,8 @@ class ClickTtPlayerRegistrationService {
this._attachNetworkLogging(page, trace);
await this._openAuthenticatedClickTt(page, { username, password, trace });
await this._selectClubContext(page, associationMemberNumber, trace);
await this._ensureApplicationEntryVisible(page, trace);
await this._clickByText(page, 'Spielberechtigungen beantragen', trace);
await this._fillSearchForm(page, memberJson);
await this._clickByText(page, 'Personen suchen', trace);
@@ -271,10 +280,13 @@ class ClickTtPlayerRegistrationService {
async _dismissConsentOverlays(page, trace) {
const candidates = [
page.getByRole('button', { name: /verstanden/i }).first(),
page.getByRole('button', { name: /akzeptieren|accept|zustimmen/i }).first(),
page.getByRole('link', { name: /verstanden/i }).first(),
page.locator('#cmpwelcomebtnyes').first(),
page.locator('[id*="cmp"][class*="button"]:has-text("Akzeptieren")').first(),
page.locator('button:has-text("Akzeptieren")').first()
page.locator('button:has-text("Akzeptieren")').first(),
page.locator('a:has-text("Verstanden")').first()
];
for (const locator of candidates) {
@@ -293,6 +305,47 @@ class ClickTtPlayerRegistrationService {
return false;
}
async _ensureApplicationEntryVisible(page, trace) {
await this._dismissConsentOverlays(page, trace);
const directEntry = await this._hasTextTarget(page, 'Spielberechtigungen beantragen');
if (directEntry) {
return;
}
if (await this._hasTextTarget(page, 'Persönlicher Bereich')) {
this._trace(trace, 'step', {
name: 'open-personal-area'
});
await this._clickByText(page, 'Persönlicher Bereich', trace);
await page.waitForLoadState('domcontentloaded');
await this._dismissConsentOverlays(page, trace);
}
}
async _selectClubContext(page, associationMemberNumber, trace) {
await this._dismissConsentOverlays(page, trace);
const clubLink = page.locator(`a:has-text("(${associationMemberNumber})")`).first();
if (await clubLink.count()) {
this._trace(trace, 'step', {
name: 'select-club-context',
associationMemberNumber
});
await clubLink.click();
await page.waitForLoadState('domcontentloaded');
await this._dismissConsentOverlays(page, trace);
return true;
}
this._trace(trace, 'step', {
name: 'club-context-not-found-on-page',
associationMemberNumber,
url: page.url()
});
return false;
}
async _fillSearchForm(page, member) {
await this._fillFirstAvailable(page, [
'input[name*=".1"]',
@@ -384,6 +437,26 @@ class ClickTtPlayerRegistrationService {
return false;
}
async _hasTextTarget(page, text) {
const escaped = escapeRegExp(text);
const selectors = [
`input[type="submit"][value*="${text}"]`,
`input[type="button"][value*="${text}"]`,
`button:has-text("${text}")`,
`a:has-text("${text}")`,
`text=/${escaped}/i`
];
for (const selector of selectors) {
const locator = page.locator(selector).first();
if (await locator.count()) {
return true;
}
}
return false;
}
async _clickByText(page, text, trace) {
const escaped = escapeRegExp(text);
const selectors = [