18 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 14-rss-email-alert-ingestion-module-rollout | 02 | execute | 1 |
|
true |
|
|
Purpose: Lowest-risk new source that proves the "add a source" pattern end-to-end and seeds the shared pollGranularity mechanism that the email slice (14-03) reuses.
Output: Working RSS adapter + normalizer dispatch, TenderRssFeedSource model + admin CRUD + UI, and a tick-driven poll path.
Phase Goal (MVP user story)
As a platform admin, I want to register RSS feed URLs (service.bund.de, a subreport-elvis municipality feed), so that their tenders appear automatically in the shared results list.
Slice progression: Task 1 proves parsing against real feed shapes (failing-first fixture test), Task 2 makes polling real (schema + CRUD + tick gate + wiring), Task 3 gives admins the UI to manage feeds.
Artifacts this phase produces
apps/api/src/tenders/adapters/rss.adapter.ts:RssAdapter implements TenderSourceAdapter,sourceType: 'rss',portals: ['rss']; pureparseFeed(xml, feedLabel): RawTenderRecord[];fetchTenders()internal fan-out overTenderRssFeedSource.findMany({where:{isActive:true}}).- Fixtures
service-bund-feed.xml,subreport-elvis-feed.xml(live-captured full XML). SourceTypeunion extended with'rss'(tender.types.ts).normalize()case 'rss'→normalizeBag()(tender-normalizer.service.ts).- Prisma
model TenderRssFeedSource { id, url @unique, label, isActive, createdAt, updatedAt }(global, no tenantId) +TenderSourcePollConfig.pollGranularity String @default("day"). TenderRssFeedSourceService(list/create/remove) with hostname denylist guard;TenderRssFeedDto.- Controller routes
GET/POST/DELETE /modules/tender-radar/rss-feeds(@Roles(ADMIN, SUPER_ADMIN)). - Web:
RssFeedListForm.tsx,tender-radar-api.tsRSS feed client fns, settings page "RSS-Feeds" section. pollDueSourcespollGranularitybranch (tick vs day).
<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/14-rss-email-alert-ingestion-module-rollout/14-CONTEXT.md @.planning/phases/14-rss-email-alert-ingestion-module-rollout/14-RESEARCH.md @apps/api/src/tenders/adapters/cosinex.adapter.ts @apps/api/src/tenders/adapters/cosinex.adapter.spec.ts Task 1: RssAdapter.parseFeed + 'rss' normalizer dispatch (fixture-first) apps/api/src/tenders/adapters/rss.adapter.ts, apps/api/src/tenders/adapters/rss.adapter.spec.ts, apps/api/src/tenders/__fixtures__/service-bund-feed.xml, apps/api/src/tenders/__fixtures__/subreport-elvis-feed.xml, apps/api/src/tenders/tender.types.ts, apps/api/src/tenders/tender-normalizer.service.ts, apps/api/src/tenders/tender-normalizer.service.spec.ts - apps/api/src/tenders/adapters/cosinex.adapter.ts (adapter shape, per-item try/catch, bag ocdsPayload, native-fetch+AbortController convention) - apps/api/src/tenders/adapters/cosinex.adapter.spec.ts (fixture/mock spec style to mirror) - apps/api/src/tenders/adapters/doe-opendata.adapter.ts §63-66 (XMLParser config: removeNSPrefix/ignoreAttributes/attributeNamePrefix) - apps/api/src/tenders/tender.types.ts (SourceType union, RawTenderRecord shape) - apps/api/src/tenders/tender-normalizer.service.ts §53-64,§125-147 (normalize switch + normalizeBag bag shape {title,buyerName,procedureType,legalFramework,deadlineAt}) - RESEARCH.md "RSS parsing" code example + Pattern 3 (item→bag mapping) + live feed shapes (service.bund.de with pubDate; subreport-elvis without, guid isPermaLink=false) - parseFeed(serviceBundXml, 'service-bund') returns one record per : sourceType 'rss', sourcePortal 'service-bund', sourceNoticeId = guid (or sha256(link).slice(0,40) when guid absent), sourceUrl = link, publishedAt from pubDate, ocdsPayload bag with title (CDATA-merged) and deadlineAt null. - parseFeed(subreportElvisXml, 'subreport-neuss') returns records with publishedAt null (no item pubDate) and title from the CDATA