Signal Pool Controller Migration Guide
Overview
The signal-pool.controller.ts needs to be updated to use the new unified status system where:
SignalPoolEntrytable is removedSignal.isPublicflag replaces pool entriesSignalPoolViewandSignalPoolRequestnow 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_expiredor custom recycling status - Track in
statusHistory
Recommend Option 1 for simplicity. The new status system handles the flow better.
Summary of Changes
| Current Approach | New Approach |
|---|---|
SignalPoolEntry table | Signal.isPublic flag |
poolEntry.status | Signal.status |
poolEntry.viewCount | Signal.viewCount |
poolEntry.requestCount | Signal.requestCount |
poolEntry.visibilityLevel | Signal.visibilityLevel |
poolEntry.publishedAt | Signal.publishedAt |
poolEntry.expiresAt | Signal.expiresAt |
poolRequest.poolEntryId | poolRequest.signalId |
poolView.poolEntryId | poolView.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
- Update controller methods one by one
- Test each endpoint after updating
- Remove recycling queue endpoints (or adapt)
- Update any tests
- Deploy to staging and test end-to-end