diff --git a/src/infrastructure/feed-mapper.test.ts b/src/infrastructure/feed-mapper.test.ts index 17f4bbc..e05906b 100644 --- a/src/infrastructure/feed-mapper.test.ts +++ b/src/infrastructure/feed-mapper.test.ts @@ -43,6 +43,7 @@ describe("feed-mapper", () => { description: "desc", mailbox_id: "a.b.42", expires_at: 3000, + pendingConfirmation: false, }); }); }); diff --git a/src/infrastructure/feed-mapper.ts b/src/infrastructure/feed-mapper.ts index ec5188b..e2d2ec7 100644 --- a/src/infrastructure/feed-mapper.ts +++ b/src/infrastructure/feed-mapper.ts @@ -43,12 +43,17 @@ export function toConfigDTO(state: FeedState): FeedConfig { } /** Domain state → the projection cached in the global `feeds:list` registry. */ -export function toListItemDTO(id: FeedId, state: FeedState): FeedListItem { +export function toListItemDTO( + id: FeedId, + state: FeedState, + pendingConfirmation = false, +): FeedListItem { return { id: id.value, title: state.title, description: state.description, mailbox_id: state.mailboxId, expires_at: state.expiresAt, + ...(pendingConfirmation !== undefined ? { pendingConfirmation } : {}), }; } diff --git a/src/infrastructure/feed-repository.test.ts b/src/infrastructure/feed-repository.test.ts index 01641df..4563419 100644 --- a/src/infrastructure/feed-repository.test.ts +++ b/src/infrastructure/feed-repository.test.ts @@ -214,3 +214,68 @@ describe("FeedRepository feed list", () => { ); }); }); + +describe("FeedRepository pendingConfirmation projection", () => { + function makeFeed(): Feed { + return Feed.create( + FeedId.generate(), + { + title: "T", + description: "", + language: "en", + allowedSenders: [], + blockedSenders: [], + }, + { mailboxId: MailboxId.unchecked("alpha.beta.11") }, + ); + } + + it("saveMetadata projects pendingConfirmation into feeds:list", async () => { + const repo = new FeedRepository(mockEnv().EMAIL_STORAGE); + const feed = makeFeed(); + await repo.save(feed); + + feed.ingest( + { + key: "k1", + subject: "s", + receivedAt: Date.now(), + size: 10, + confirmation: { links: ["https://x/confirm"] }, + }, + { maxBytes: 1_000_000 }, + ); + await repo.saveMetadata(feed); + + const list = await repo.listFeeds(); + const entry = list.find((f) => f.id === feed.id.value); + expect(entry?.pendingConfirmation).toBe(true); + }); + + it("saveMetadata clears the projected flag after dismiss", async () => { + const repo = new FeedRepository(mockEnv().EMAIL_STORAGE); + const feed = makeFeed(); + feed.ingest( + { + key: "k1", + subject: "s", + receivedAt: Date.now(), + size: 10, + confirmation: { links: ["https://x/confirm"] }, + }, + { maxBytes: 1_000_000 }, + ); + await repo.save(feed); + expect( + (await repo.listFeeds()).find((f) => f.id === feed.id.value) + ?.pendingConfirmation, + ).toBe(true); + + feed.dismissConfirmation(); + await repo.saveMetadata(feed); + expect( + (await repo.listFeeds()).find((f) => f.id === feed.id.value) + ?.pendingConfirmation, + ).toBe(false); + }); +}); diff --git a/src/infrastructure/feed-repository.ts b/src/infrastructure/feed-repository.ts index 2b52112..90cd6b3 100644 --- a/src/infrastructure/feed-repository.ts +++ b/src/infrastructure/feed-repository.ts @@ -87,18 +87,26 @@ export class FeedRepository { await Promise.all([ this.putConfig(feed.id, toConfigDTO(feed.state())), this.putMetadata(feed.id, feed.toMetadataSnapshot()), - this.upsertListEntry(toListItemDTO(feed.id, feed.state())), + this.upsertListEntry( + toListItemDTO(feed.id, feed.state(), feed.pendingConfirmation), + ), this.putInboundIndex(feed.mailboxId, feed.id), ]); } /** * Persist only the email index. Used by the ingest/delete paths where config - * is unchanged — avoids a redundant config write on the hot path. The list - * projection (title/description/expiry) is untouched, so it is not rewritten. + * is unchanged — avoids a redundant config write on the hot path. Also + * refreshes the `feeds:list` entry's `pendingConfirmation` projection so the + * dashboard reflects the latest flag state with a single subsequent KV read. */ async saveMetadata(feed: Feed): Promise { - await this.putMetadata(feed.id, feed.toMetadataSnapshot()); + await Promise.all([ + this.putMetadata(feed.id, feed.toMetadataSnapshot()), + this.upsertListEntry( + toListItemDTO(feed.id, feed.state(), feed.pendingConfirmation), + ), + ]); } /** @@ -109,7 +117,9 @@ export class FeedRepository { async saveConfig(feed: Feed): Promise { await Promise.all([ this.putConfig(feed.id, toConfigDTO(feed.state())), - this.upsertListEntry(toListItemDTO(feed.id, feed.state())), + this.upsertListEntry( + toListItemDTO(feed.id, feed.state(), feed.pendingConfirmation), + ), this.putInboundIndex(feed.mailboxId, feed.id), ]); }