diff --git a/apps/api/src/tenders/tenders.controller.spec.ts b/apps/api/src/tenders/tenders.controller.spec.ts index c9d41f6..3c7e9fb 100644 --- a/apps/api/src/tenders/tenders.controller.spec.ts +++ b/apps/api/src/tenders/tenders.controller.spec.ts @@ -24,6 +24,18 @@ function makeFakePrisma() { title: 'Beispielausschreibung', status: 'active', sourcePortal: 'doe-opendata', + sources: [ + { + sourcePortal: 'doe-opendata', + sourceUrl: 'https://doe.example/t1', + sourceNoticeId: 'doe-1', + }, + { + sourcePortal: 'cosinex-dtvp', + sourceUrl: 'https://dtvp.example/t1', + sourceNoticeId: 'dtvp-1', + }, + ], }); const configs = new Map(); configs.set('doe-opendata', { @@ -147,6 +159,40 @@ describe('TendersController — global read (not tenant-scoped)', () => { expect(callArgs.where).toEqual({ id: 't1' }); }); + it('GET /:id includes the sources relation so the response carries all TenderSource links (13-06, SCHEMA-03)', async () => { + const prisma = makeFakePrisma(); + const scheduler = { setInterval: vi.fn(), stopJob: vi.fn() } as any; + const triageService = makeFakeTriageService(); + const controller = new TendersController( + prisma as any, + scheduler, + triageService as any, + makeFakeSavedSearchService() as any, + makeFakeNotificationPrefService() as any, + ); + + const result = await controller.getTender('t1'); + + const callArgs = prisma.tender.findUnique.mock.calls[0][0]; + expect(callArgs.include).toEqual({ + sources: { + select: { sourcePortal: true, sourceUrl: true, sourceNoticeId: true }, + }, + }); + expect(result.sources).toEqual([ + { + sourcePortal: 'doe-opendata', + sourceUrl: 'https://doe.example/t1', + sourceNoticeId: 'doe-1', + }, + { + sourcePortal: 'cosinex-dtvp', + sourceUrl: 'https://dtvp.example/t1', + sourceNoticeId: 'dtvp-1', + }, + ]); + }); + it('GET /:id throws NotFoundException for a missing id', async () => { const prisma = makeFakePrisma(); const scheduler = { setInterval: vi.fn(), stopJob: vi.fn() } as any; diff --git a/apps/api/src/tenders/tenders.controller.ts b/apps/api/src/tenders/tenders.controller.ts index f82826c..8416ce3 100644 --- a/apps/api/src/tenders/tenders.controller.ts +++ b/apps/api/src/tenders/tenders.controller.ts @@ -343,11 +343,25 @@ export class TendersController { * GET /modules/tender-radar/:id — single tender detail. * Gated by @UseModule('tender-radar'); NOT scoped by the tenant's id * (global row). + * + * D-03/SCHEMA-03 (13-06): includes the `sources` relation (TenderSource + * rows) so a cross-source-deduplicated tender surfaces links to ALL of + * its source portals, not just the single primary `sourceUrl` column. + * `select` is scoped to the three display fields the frontend needs — + * no `id`/`createdAt` leak, same minimal-surface convention as + * `getCoverage`'s groupBy projection. */ @Get(':id') @UseModule('tender-radar') async getTender(@Param('id') id: string) { - const tender = await this.prisma.tender.findUnique({ where: { id } }); + const tender = await this.prisma.tender.findUnique({ + where: { id }, + include: { + sources: { + select: { sourcePortal: true, sourceUrl: true, sourceNoticeId: true }, + }, + }, + }); if (!tender) { throw new NotFoundException('Tender not found'); }