Skip to main content

Signal Pool Controller Migration Guide

Overview

The signal-pool.controller.ts needs to be updated to use the new unified status system where:

  • SignalPoolEntry table is removed
  • Signal.isPublic flag replaces pool entries
  • SignalPoolView and SignalPoolRequest now reference Signal directly

Key Changes Needed

1. Replace SignalPoolEntry Queries

Old approach (used throughout current controller):

const poolEntry = await this.prisma.signalPoolEntry.findUnique({
where: { signalId },
});

New approach:

const signal = await this.prisma.signal.findUnique({
where: {
id: signalId,
isPublic: true, // Only pool signals
},
});

2. Update addToPool Endpoint (Line 142)

Current: Creates SignalPoolEntry New: Update Signal with isPublic = true

async addToPool(
@Param('signalId') signalId: string,
@Body() dto: AddToPoolDto,
@Request() req: any,
) {
const signal = await this.prisma.signal.findUnique({
where: { id: signalId },
});

if (!signal) {
throw new NotFoundException(`Signal with ID ${signalId} not found`);
}

if (signal.isPublic) {
throw new ConflictException('Signal is already in the pool');
}

// Update signal to be public
const updatedSignal = await this.prisma.signal.update({
where: { id: signalId },
data: {
isPublic: true,
status: 'signal_assigned',
publishedAt: new Date(),
expiresAt: dto.expiresAt ? new Date(dto.expiresAt) : null,
visibilityLevel: dto.visibilityLevel || 'all',
viewCount: 0,
requestCount: 0,
},
});

return {
success: true,
data: updatedSignal,
message: 'Signal added to pool successfully',
};
}

3. Update removeFromPool Endpoint (Line 262)

Current: Deletes SignalPoolEntry New: Update Signal.isPublic = false

async removeFromPool(
@Param('signalId') signalId: string,
@Request() req: any,
) {
const signal = await this.prisma.signal.findUnique({
where: { id: signalId },
});

if (!signal || !signal.isPublic) {
throw new NotFoundException('Signal not found in pool');
}

await this.prisma.signal.update({
where: { id: signalId },
data: {
isPublic: false,
status: 'signal_unassigned',
publishedAt: null,
expiresAt: null,
},
});

return {
success: true,
message: 'Signal removed from pool successfully',
};
}

4. Update getPoolSignalsAdmin Endpoint (Line 359)

Current: Queries signalPoolEntry table New: Query signal table with isPublic = true

async getPoolSignalsAdmin(
@Query('page') page: string = '1',
@Query('limit') limit: string = '100',
@Query('status') status?: string,
) {
const pageNum = parseInt(page, 10) || 1;
const limitNum = parseInt(limit, 10) || 100;
const skip = (pageNum - 1) * limitNum;

const where: any = {
isPublic: true,
isActive: true,
};

if (status && status !== 'all') {
where.status = status;
}

const [total, signals] = await Promise.all([
this.prisma.signal.count({ where }),
this.prisma.signal.findMany({
where,
skip,
take: limitNum,
orderBy: { publishedAt: 'desc' },
include: {
government: true,
governmentContact: true,
signalMarkers: true,
poolRequests: {
include: { startup: true },
orderBy: { createdAt: 'desc' },
},
poolViews: true,
},
}),
]);

return {
success: true,
data: {
data: signals,
pagination: {
page: pageNum,
limit: limitNum,
total,
pages: Math.ceil(total / limitNum),
},
},
};
}

5. Update getPoolSignals Endpoint (Line 454)

Current: Queries signalPoolEntry with complex filtering New: Query signal with isPublic = true

async getPoolSignals(
@Query('urgency') urgency?: string,
@Query('search') search?: string,
@Query('page') page: string = '1',
@Query('limit') limit: string = '20',
@Request() req?: any,
) {
const pageNum = parseInt(page, 10) || 1;
const limitNum = parseInt(limit, 10) || 20;
const skip = (pageNum - 1) * limitNum;

const userEmail = req?.auth?.emailAddress;
let startupId: string | null = null;

if (userEmail) {
const startup = await this.prisma.startupContact.findFirst({
where: { contactEmail: { equals: userEmail, mode: 'insensitive' } },
});
startupId = startup?.id || null;
}

const where: any = {
isPublic: true,
isActive: true,
isArchived: false,
OR: [
{ expiresAt: null },
{ expiresAt: { gt: new Date() } }
],
};

if (urgency) {
where.urgency = urgency;
}

if (search) {
where.OR = [
{ title: { contains: search, mode: 'insensitive' } },
{ painPoints: { contains: search, mode: 'insensitive' } },
{ description: { contains: search, mode: 'insensitive' } },
];
}

const [total, signals] = await Promise.all([
this.prisma.signal.count({ where }),
this.prisma.signal.findMany({
where,
skip,
take: limitNum,
orderBy: { publishedAt: 'desc' },
include: {
government: true,
governmentContact: {
select: {
id: true,
contactName: true,
jobTitle: true,
// Hide email/phone until approved
},
},
signalMarkers: true,
poolViews: startupId ? {
where: { startupId },
} : undefined,
poolRequests: startupId ? {
where: { startupId },
} : undefined,
},
}),
]);

return {
success: true,
data: {
signals,
pagination: {
page: pageNum,
limit: limitNum,
total,
pages: Math.ceil(total / limitNum),
},
},
};
}

6. Update createPoolRequest Endpoint (Line 598)

Current: References poolEntryId New: Reference signalId directly

async createPoolRequest(
@Param('signalId') signalId: string,
@Body() dto: CreatePoolRequestDto,
@Request() req: any,
) {
const userEmail = req.auth?.emailAddress;
const userId = req.auth?.userId;

const startup = await this.prisma.startupContact.findFirst({
where: { contactEmail: { equals: userEmail, mode: 'insensitive' } },
});

if (!startup) {
throw new NotFoundException('Startup profile not found');
}

// Verify signal is public
const signal = await this.prisma.signal.findUnique({
where: { id: signalId },
});

if (!signal || !signal.isPublic || !signal.isActive) {
throw new NotFoundException('Signal not available in pool');
}

// Check for existing request
const existing = await this.prisma.signalPoolRequest.findUnique({
where: {
signalId_startupId: {
signalId,
startupId: startup.id,
},
},
});

if (existing && existing.status !== 'rejected') {
throw new ConflictException('Request already exists for this signal');
}

// Create request
const request = await this.prisma.signalPoolRequest.create({
data: {
signalId, // Direct reference now
startupId: startup.id,
requestedBy: userId,
status: 'pending',
justification: dto.justification,
},
include: {
startup: true,
signal: {
include: {
government: true,
governmentContact: true,
},
},
},
});

// Increment request count
await this.prisma.signal.update({
where: { id: signalId },
data: { requestCount: { increment: 1 } },
});

return {
success: true,
data: request,
message: 'Request submitted successfully',
};
}

7. Update recordView Endpoint (Line 992)

Current: References poolEntryId New: Reference signalId directly

async recordView(
@Param('signalId') signalId: string,
@Request() req: any,
) {
const userEmail = req.auth?.emailAddress;

const signal = await this.prisma.signal.findUnique({
where: { id: signalId },
});

if (!signal || !signal.isPublic) {
throw new NotFoundException('Signal not in pool');
}

const startup = await this.prisma.startupContact.findFirst({
where: { contactEmail: { equals: userEmail, mode: 'insensitive' } },
});

if (!startup) {
return {
success: true,
message: 'View recorded',
};
}

// Check if already viewed
const existingView = await this.prisma.signalPoolView.findUnique({
where: {
signalId_startupId: {
signalId,
startupId: startup.id,
},
},
});

if (!existingView) {
await this.prisma.$transaction([
this.prisma.signalPoolView.create({
data: {
signalId, // Direct reference now
startupId: startup.id,
},
}),
this.prisma.signal.update({
where: { id: signalId },
data: { viewCount: { increment: 1 } },
}),
]);
}

return {
success: true,
message: 'View recorded',
};
}

8. Remove/Update Recycling Queue Endpoints

The recycling queue functionality (lines 1194-1354) was specific to SignalPoolEntry.status. With the new system:

Option 1: Remove recycling queue entirely (simpler)

  • Just archive signals that don't work out
  • Or use signal status transitions directly

Option 2: Implement using signal status

  • Use signal_expired or custom recycling status
  • Track in statusHistory

Recommend Option 1 for simplicity. The new status system handles the flow better.

Summary of Changes

Current ApproachNew Approach
SignalPoolEntry tableSignal.isPublic flag
poolEntry.statusSignal.status
poolEntry.viewCountSignal.viewCount
poolEntry.requestCountSignal.requestCount
poolEntry.visibilityLevelSignal.visibilityLevel
poolEntry.publishedAtSignal.publishedAt
poolEntry.expiresAtSignal.expiresAt
poolRequest.poolEntryIdpoolRequest.signalId
poolView.poolEntryIdpoolView.signalId

Testing Checklist

After updates, test:

  • Admin can publish signal to pool
  • Admin can remove signal from pool
  • Admin can view all pool signals
  • Startups can view available pool signals
  • Startups can request pool signals
  • View tracking works correctly
  • Request count increments properly
  • Expired signals are filtered out
  • Visibility levels work correctly

Migration Steps

  1. Update controller methods one by one
  2. Test each endpoint after updating
  3. Remove recycling queue endpoints (or adapt)
  4. Update any tests
  5. Deploy to staging and test end-to-end