Compare commits

..

2 Commits

Author SHA1 Message Date
maaz519 18498ee9fa Merge pull request 'feat(mail): tag mail threads source=crm-mail (list separately from chat)' (#3) from feat/s3-storage into dev
Reviewed-on: #3
2026-07-18 11:12:35 +00:00
maaz519 40c98522dc feat(mail): tag mail threads source=crm-mail (list separately from chat)
ingest() puts metadata on the interaction, not the thread, and listThreads filters
THREAD metadata — so MailService now tags the thread directly after deposit
(source=crm-mail). Lets the CRM list mail threads apart from Messenger chat
(source=crm-messenger). 7 tests. NOTE: requires an IIOS redeploy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 16:30:28 +05:30
2 changed files with 15 additions and 1 deletions
@@ -25,7 +25,7 @@ function mail(): MailService {
const ingest = new IngestService(asService, makeFakePorts(), actors);
const outbound = new OutboundService(asService, new CapabilityBroker(makeFakePorts(), new CapabilityProviderRegistry()), new IdempotencyService(asService));
const sender = new TemplatedSender(templates, outbound, makeFakePorts());
return new MailService(templates, ingest, sender, actors);
return new MailService(templates, ingest, sender, actors, asService);
}
const messages = () => new MessageService(asService, makeFakePorts(), actors);
@@ -56,6 +56,7 @@ describe('MailService.postInternal', () => {
const thread = await prisma.iiosThread.findUniqueOrThrow({ where: { id: res.threadId } });
expect(thread.subject).toBe('Welcome Dana');
expect((thread.metadata as { source?: string } | null)?.source).toBe('crm-mail'); // lists separately from chat
// The load-bearing assertion: the RECIPIENT can see the thread in their inbox.
const bobThreads = await messages().listThreads(bob);
@@ -1,4 +1,6 @@
import { Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { IngestService } from '../interactions/ingest.service';
import { ActorResolver, type MessagePrincipal } from '../identity/actor.resolver';
import { TemplateService } from '../templates/template.service';
@@ -55,8 +57,12 @@ export class MailService {
private readonly ingest: IngestService,
private readonly sender: TemplatedSender,
private readonly actors: ActorResolver,
private readonly prisma: PrismaService,
) {}
/** Marks a thread as CRM mail so it lists separately from chat (Messenger uses source=crm-messenger). */
static readonly SOURCE = 'crm-mail';
async postInternal(principal: MessagePrincipal, input: PostInternalInput): Promise<{ threadId: string; interactionId: string }> {
const { content } = await this.templates.render(input.source, input.vars ?? {}, { channel: 'INTERNAL', ...(input.locale ? { locale: input.locale } : {}) });
return this.deposit(principal, input.recipientUserId, content, input.idempotencyKey, 'PORTAL');
@@ -103,6 +109,13 @@ export class MailService {
await this.actors.ensureParticipant(res.threadId, senderActor.id);
await this.actors.ensureParticipant(res.threadId, recipientActor.id);
// Tag the thread as CRM mail (thread metadata) so it lists separately from Messenger chat.
// ingest() puts req.metadata on the interaction, not the thread, so we set it here directly.
await this.prisma.iiosThread.update({
where: { id: res.threadId },
data: { metadata: { source: MailService.SOURCE } as Prisma.InputJsonValue },
});
return { threadId: res.threadId, interactionId: res.interactionId };
}
}