import { Injectable } from '@nestjs/common'; import type { SourceType } from './tender.types'; import type { TenderSourceAdapter } from './adapters/tender-source-adapter.interface'; /** * Portals whose AGB (Nutzungsbedingungen) prohibit automated/scraping * access (INGEST-07/D-06). This is the hard code boundary — NOT just * documentation: `SourceRegistry.register()` checks every adapter's * `portals` against this list and refuses registration outright if any * entry matches, at DI-boot time, before any poll can run. */ export const DENYLISTED_PORTALS = ['vergabe24', 'aumass'] as const; /** * Canonical direct-link URL per denylisted portal (UI-06/D-12). Keyed by * the same portal slugs as `DENYLISTED_PORTALS` — the portal SET is never * re-declared here, only the URL each already-listed portal maps to. * `TendersController.getDenylistedPortals()` maps over `DENYLISTED_PORTALS` * and looks up each entry's URL here, so a future denylist addition needs * only a URL added to this map (not a second hardcoded list anywhere else). */ export const PORTAL_URLS: Record<(typeof DENYLISTED_PORTALS)[number], string> = { vergabe24: 'https://www.vergabe24.de', aumass: 'https://www.aumass.de', }; /** * Thrown by `SourceRegistry.register()` when an adapter declares a * denylisted portal. Proves Erfolgskriterium 4 (13-CONTEXT.md D-06) — * see source-registry.spec.ts. */ export class DeniedPortalError extends Error { constructor(portal: string) { super( `Portal '${portal}' ist AGB-seitig für automatisierten Zugriff gesperrt und darf nicht registriert werden.`, ); this.name = 'DeniedPortalError'; } } /** * Central registry mapping `SourceType` -> `TenderSourceAdapter`, gated by * the AGB denylist (T-13-02-01/T-13-02-02). `pollDueSources` (Plan 13-03) * uses `get()`/`activeAdapters()` to fan out over all registered sources * (poll-once-fan-out-many) instead of hardwiring a single adapter. */ @Injectable() export class SourceRegistry { private readonly adapters = new Map(); /** * Registers an adapter. Iterates ALL of the adapter's declared `portals` * — a single denylisted entry rejects the whole adapter, even if the * rest of its portals are legitimate (T-13-02-02: no partial/mixed * registration). */ register(adapter: TenderSourceAdapter): void { for (const portal of adapter.portals) { if ((DENYLISTED_PORTALS as readonly string[]).includes(portal)) { throw new DeniedPortalError(portal); } } this.adapters.set(adapter.sourceType, adapter); } /** Returns the adapter for a sourceType, or undefined if none is registered — never throws. */ get(type: SourceType): TenderSourceAdapter | undefined { return this.adapters.get(type); } /** All currently registered adapters, for fan-out scheduling. */ activeAdapters(): TenderSourceAdapter[] { return [...this.adapters.values()]; } }