2026-04-29 02:48:43 +02:00
|
|
|
import { randomUUID } from 'crypto';
|
|
|
|
|
import { and, eq } from 'drizzle-orm';
|
|
|
|
|
|
|
|
|
|
import { db } from '@/lib/db';
|
|
|
|
|
import { interests } from '@/lib/db/schema/interests';
|
|
|
|
|
import { clients } from '@/lib/db/schema/clients';
|
|
|
|
|
import { ValidationError, ForbiddenError } from '@/lib/errors';
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
import { getQueue } from '@/lib/queue';
|
|
|
|
|
|
|
|
|
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
export interface DraftRequest {
|
|
|
|
|
interestId: string;
|
|
|
|
|
clientId: string;
|
|
|
|
|
portId: string;
|
|
|
|
|
context: 'follow_up' | 'introduction' | 'stage_update' | 'general';
|
|
|
|
|
additionalInstructions?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface DraftResult {
|
|
|
|
|
subject: string;
|
|
|
|
|
body: string;
|
|
|
|
|
generatedAt: Date;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Request draft (enqueues job) ─────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Request an AI-generated email draft.
|
2026-04-29 02:48:43 +02:00
|
|
|
*
|
|
|
|
|
* Generates an opaque random jobId rather than relying on BullMQ's default
|
2026-05-04 22:57:01 +02:00
|
|
|
* sequential ids - the jobId is the access token for polling, so it must
|
2026-04-29 02:48:43 +02:00
|
|
|
* not be enumerable. The job payload also captures the requesting user
|
|
|
|
|
* + port so the poll endpoint can refuse cross-tenant / cross-user reads.
|
|
|
|
|
*
|
|
|
|
|
* The interestId and clientId are validated against portId before enqueue
|
|
|
|
|
* so a port-A caller cannot trigger a draft built from port-B data.
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
*/
|
|
|
|
|
export async function requestEmailDraft(
|
|
|
|
|
userId: string,
|
|
|
|
|
request: DraftRequest,
|
|
|
|
|
): Promise<{ jobId: string }> {
|
2026-04-29 02:48:43 +02:00
|
|
|
const interest = await db.query.interests.findFirst({
|
|
|
|
|
where: and(eq(interests.id, request.interestId), eq(interests.portId, request.portId)),
|
|
|
|
|
});
|
|
|
|
|
if (!interest) {
|
|
|
|
|
throw new ValidationError('interestId not found in this port');
|
|
|
|
|
}
|
|
|
|
|
const client = await db.query.clients.findFirst({
|
|
|
|
|
where: and(eq(clients.id, request.clientId), eq(clients.portId, request.portId)),
|
|
|
|
|
});
|
|
|
|
|
if (!client) {
|
|
|
|
|
throw new ValidationError('clientId not found in this port');
|
|
|
|
|
}
|
|
|
|
|
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
const aiQueue = getQueue('ai');
|
2026-04-29 02:48:43 +02:00
|
|
|
const jobId = randomUUID();
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
|
2026-04-29 02:48:43 +02:00
|
|
|
await aiQueue.add(
|
|
|
|
|
'generate-email-draft',
|
|
|
|
|
{
|
2026-05-04 22:57:01 +02:00
|
|
|
// No PII - only IDs and context parameters
|
2026-04-29 02:48:43 +02:00
|
|
|
interestId: request.interestId,
|
|
|
|
|
clientId: request.clientId,
|
|
|
|
|
portId: request.portId,
|
|
|
|
|
context: request.context,
|
|
|
|
|
additionalInstructions: request.additionalInstructions,
|
|
|
|
|
requestedBy: userId,
|
|
|
|
|
},
|
|
|
|
|
{ jobId },
|
|
|
|
|
);
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
|
2026-04-29 02:48:43 +02:00
|
|
|
return { jobId };
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Poll for result ──────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the result of an email draft generation job.
|
|
|
|
|
* Returns null if still processing.
|
2026-04-29 02:48:43 +02:00
|
|
|
*
|
|
|
|
|
* Verifies the caller (userId + portId) matches the job's original
|
|
|
|
|
* requester before returning the drafted subject/body. A foreign caller
|
|
|
|
|
* who happens to know the jobId (or stumbles on it) sees null, not the
|
|
|
|
|
* drafted content.
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
*/
|
2026-04-29 02:48:43 +02:00
|
|
|
export async function getEmailDraftResult(
|
|
|
|
|
jobId: string,
|
|
|
|
|
caller: { userId: string; portId: string },
|
|
|
|
|
): Promise<DraftResult | null> {
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
const aiQueue = getQueue('ai');
|
|
|
|
|
const job = await aiQueue.getJob(jobId);
|
|
|
|
|
|
|
|
|
|
if (!job) return null;
|
|
|
|
|
|
2026-04-29 02:48:43 +02:00
|
|
|
const data = job.data as { requestedBy?: string; portId?: string } | undefined | null;
|
|
|
|
|
if (!data || data.requestedBy !== caller.userId || data.portId !== caller.portId) {
|
|
|
|
|
throw new ForbiddenError('Email draft not accessible');
|
|
|
|
|
}
|
|
|
|
|
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
const state = await job.getState();
|
|
|
|
|
|
|
|
|
|
if (state !== 'completed') return null;
|
|
|
|
|
|
|
|
|
|
const returnValue = job.returnvalue as
|
|
|
|
|
| { subject: string; body: string; generatedAt: string }
|
|
|
|
|
| undefined
|
|
|
|
|
| null;
|
|
|
|
|
|
|
|
|
|
if (!returnValue) return null;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
subject: returnValue.subject,
|
|
|
|
|
body: returnValue.body,
|
|
|
|
|
generatedAt: new Date(returnValue.generatedAt),
|
|
|
|
|
};
|
|
|
|
|
}
|