feat(13-06): getTender include sources[] (SCHEMA-03 read surface)
GET /modules/tender-radar/:id now includes the TenderSource relation (sourcePortal, sourceUrl, sourceNoticeId) so a cross-source-deduped tender's detail response carries links to all its source portals, not just the single primary sourceUrl column. Route order unchanged (:id stays after all static routes). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,18 @@ function makeFakePrisma() {
|
|||||||
title: 'Beispielausschreibung',
|
title: 'Beispielausschreibung',
|
||||||
status: 'active',
|
status: 'active',
|
||||||
sourcePortal: 'doe-opendata',
|
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<string, any>();
|
const configs = new Map<string, any>();
|
||||||
configs.set('doe-opendata', {
|
configs.set('doe-opendata', {
|
||||||
@@ -147,6 +159,40 @@ describe('TendersController — global read (not tenant-scoped)', () => {
|
|||||||
expect(callArgs.where).toEqual({ id: 't1' });
|
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 () => {
|
it('GET /:id throws NotFoundException for a missing id', async () => {
|
||||||
const prisma = makeFakePrisma();
|
const prisma = makeFakePrisma();
|
||||||
const scheduler = { setInterval: vi.fn(), stopJob: vi.fn() } as any;
|
const scheduler = { setInterval: vi.fn(), stopJob: vi.fn() } as any;
|
||||||
|
|||||||
@@ -343,11 +343,25 @@ export class TendersController {
|
|||||||
* GET /modules/tender-radar/:id — single tender detail.
|
* GET /modules/tender-radar/:id — single tender detail.
|
||||||
* Gated by @UseModule('tender-radar'); NOT scoped by the tenant's id
|
* Gated by @UseModule('tender-radar'); NOT scoped by the tenant's id
|
||||||
* (global row).
|
* (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')
|
@Get(':id')
|
||||||
@UseModule('tender-radar')
|
@UseModule('tender-radar')
|
||||||
async getTender(@Param('id') id: string) {
|
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) {
|
if (!tender) {
|
||||||
throw new NotFoundException('Tender not found');
|
throw new NotFoundException('Tender not found');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user