fix(tenders): link DOE notices to the notice page, not the API
The doe-opendata adapter stored the OCDS document's own `uri` as sourceUrl. That is the API address of the record and serves OCDS JSON by design, so anyone following the link from the results list, the detail view, or an alert mail landed on raw JSON instead of the notice. Build the human-readable page from the notice id the row already carries instead. `/ui/de/search/details?noticeId=...` is the redirect target of `/ui/de/notices/...`, so it needs no redirect. Verified in a browser for both id shapes the feed uses -- numeric (25673764 -> "Feuerwehr-Geraetehaus Miehlen Fliesenarbeiten") and UUID (7085ba12-... -> "Holzfassade"). The page is a single-page app that answers 200 with an identical shell for any id, so this had to be checked on rendered content; a status code proves nothing. The adapter alone only fixes new ingests, so a backfill migration rewrites the rows already stored -- in Tender and in TenderSource, since the detail view lists per-source links separately. It touches only rows still pointing at /api/notices/ and only ids of a shape that was actually verified, which makes it idempotent and keeps an unexpected id from being pasted into a URL. Counted read-only against the live database beforehand: 2846 DOE rows affected, none skipped. Closes the 2026-08-05 backlog item, which was deliberately held until Phase 16 was done. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import AdmZip from 'adm-zip';
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { DoeOpenDataAdapter } from './doe-opendata.adapter';
|
||||
import { buildDoeNoticeUrl, DoeOpenDataAdapter } from './doe-opendata.adapter';
|
||||
|
||||
/**
|
||||
* Real, trimmed DÖE day-export fixtures (8 notices, captured live from
|
||||
@@ -129,6 +129,42 @@ describe('DoeOpenDataAdapter', () => {
|
||||
await expect(adapter.fetchTenders(TEST_PUBDAY)).rejects.toThrow();
|
||||
});
|
||||
|
||||
// Backlog item 2026-08-05: sourceUrl used to be the OCDS document's own
|
||||
// `uri`, which is the API address and answers with JSON. Users following a
|
||||
// link from the results list, the detail view or an alert mail landed on raw
|
||||
// JSON instead of the notice.
|
||||
it('points sourceUrl at the human-readable notice page, never at the API', async () => {
|
||||
stubFetchWithFixtures();
|
||||
const adapter = new DoeOpenDataAdapter();
|
||||
|
||||
const records = await adapter.fetchTenders(TEST_PUBDAY);
|
||||
|
||||
expect(records.length).toBe(EXPECTED_TENDER_TAGGED_COUNT);
|
||||
for (const record of records) {
|
||||
expect(record.sourceUrl).toBe(
|
||||
`https://oeffentlichevergabe.de/ui/de/search/details?noticeId=${record.sourceNoticeId}`,
|
||||
);
|
||||
// The exact shape that was broken — an API address serving OCDS JSON.
|
||||
expect(record.sourceUrl).not.toContain('/api/notices/');
|
||||
expect(record.sourceUrl).not.toContain('format=ocds');
|
||||
}
|
||||
});
|
||||
|
||||
it('builds the notice URL from both id shapes the feed uses, escaping the id', () => {
|
||||
// Numeric and UUID ids both occur in the live feed; both were verified in
|
||||
// a browser on 2026-08-11 to render the correct notice.
|
||||
expect(buildDoeNoticeUrl('25673764')).toBe(
|
||||
'https://oeffentlichevergabe.de/ui/de/search/details?noticeId=25673764',
|
||||
);
|
||||
expect(buildDoeNoticeUrl('7085ba12-c7f4-4f8c-8599-2fe9e4e2737c')).toBe(
|
||||
'https://oeffentlichevergabe.de/ui/de/search/details?noticeId=7085ba12-c7f4-4f8c-8599-2fe9e4e2737c',
|
||||
);
|
||||
// An id is directory data, not something to paste into a URL unchecked.
|
||||
expect(buildDoeNoticeUrl('a b&c=d')).toBe(
|
||||
'https://oeffentlichevergabe.de/ui/de/search/details?noticeId=a%20b%26c%3Dd',
|
||||
);
|
||||
});
|
||||
|
||||
it('never imports or uses axios (native fetch is the sole HTTP client convention)', () => {
|
||||
const source = readFileSync(
|
||||
join(__dirname, 'doe-opendata.adapter.ts'),
|
||||
|
||||
Reference in New Issue
Block a user