Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/pages/api/agents/listings/live-filter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { getLiveListingsCutoffDate, filterAgentEligibleListings, AgentListing } from './live-filter';

describe('Live Agent Listings Cutoff & Filtering', () => {
it('should default cutoff to start of current UTC date when deadline omitted', () => {
const cutoff = getLiveListingsCutoffDate();
expect(cutoff.getUTCHours()).toBe(0);
expect(cutoff.getUTCMinutes()).toBe(0);
expect(cutoff.getUTCSeconds()).toBe(0);
expect(cutoff.getUTCMilliseconds()).toBe(0);
});

it('should parse valid customDeadline and fallback on invalid input', () => {
const custom = getLiveListingsCutoffDate('2026-08-15T12:00:00.000Z');
expect(custom.toISOString()).toBe('2026-08-15T12:00:00.000Z');

const invalid = getLiveListingsCutoffDate('invalid-date');
expect(invalid.getUTCHours()).toBe(0);
});

it('should include both AGENT_ALLOWED and AGENT_ONLY listings while rejecting closed and past deadline items', () => {
const cutoff = new Date('2026-08-01T00:00:00.000Z');
const mockListings: AgentListing[] = [
{ id: '1', agentAccess: 'AGENT_ALLOWED', status: 'OPEN', deadline: '2026-08-28T21:59:59.000Z' },
{ id: '2', agentAccess: 'AGENT_ONLY', status: 'OPEN', deadline: '2026-08-15T00:00:00.000Z' },
{ id: '3', agentAccess: 'NONE', status: 'OPEN', deadline: '2026-08-28T21:59:59.000Z' },
{ id: '4', agentAccess: 'AGENT_ALLOWED', status: 'CLOSED', deadline: '2026-08-28T21:59:59.000Z' },
{ id: '5', agentAccess: 'AGENT_ONLY', status: 'OPEN', deadline: '2026-07-01T00:00:00.000Z' }
];

const result = filterAgentEligibleListings(mockListings, cutoff);
expect(result.length).toBe(2);
expect(result.map(r => r.id)).toEqual(['1', '2']);
});
});
27 changes: 27 additions & 0 deletions src/pages/api/agents/listings/live-filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export interface AgentListing {
readonly id: string | undefined;
readonly agentAccess: string | undefined;
readonly status: string | undefined;
readonly deadline: string | undefined;
}

export function getLiveListingsCutoffDate(customDeadline?: string): Date {
if (customDeadline) {
const parsed = new Date(customDeadline);
if (!isNaN(parsed.getTime())) return parsed;
}
const now = new Date();
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), 0, 0, 0, 0));
}

export function filterAgentEligibleListings(
listings: readonly AgentListing[],
cutoffDate: Date
): AgentListing[] {
return listings.filter((item) => {
const isAgentAllowed = item.agentAccess === "AGENT_ALLOWED" || item.agentAccess === "AGENT_ONLY";
const isOpen = item.status === "OPEN";
const isNotExpired = item.deadline ? new Date(item.deadline) >= cutoffDate : true;
return isAgentAllowed && isOpen && isNotExpired;
});
}
21 changes: 18 additions & 3 deletions src/pages/api/agents/listings/live.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import { type NextApiRequestWithAgent } from '@/features/auth/types';
import { withAgentAuth } from '@/features/auth/utils/withAgentAuth';
import { listingSelect } from '@/features/listings/constants/schema';

/**
* GET /api/agents/listings/live
* Discovery endpoint returning currently active and agent-accessible listings.
* Filters for status OPEN, agentAccess in ['AGENT_ALLOWED', 'AGENT_ONLY'], and deadline >= now.
*/
async function handler(req: NextApiRequestWithAgent, res: NextApiResponse) {
const params = req.query;

Expand All @@ -23,8 +28,18 @@ async function handler(req: NextApiRequestWithAgent, res: NextApiResponse) {
if (!takeResult.ok) {
return res.status(400).json({ error: takeResult.error });
}
const take = takeResult.value;
const deadline = params.deadline as string;
let deadlineDate: Date | undefined = new Date();
if (params.deadline !== undefined) {
if (typeof params.deadline !== 'string' || params.deadline.trim() === '') {
return res.status(400).json({ error: 'Expected ISO-8601 datetime format for deadline' });
}
const rawDeadline = params.deadline.trim();
const parsed = new Date(rawDeadline);
if (isNaN(parsed.getTime())) {
return res.status(400).json({ error: 'Expected ISO-8601 datetime format for deadline' });
}
deadlineDate = parsed;
}
const exclusiveSponsorId = params.exclusiveSponsorId as string | undefined;
let excludeIds = params['excludeIds[]'];
if (typeof excludeIds === 'string') {
Expand All @@ -41,7 +56,7 @@ async function handler(req: NextApiRequestWithAgent, res: NextApiResponse) {
isPrivate: false,
isArchived: false,
status: 'OPEN',
deadline: { gte: deadline },
deadline: deadlineDate ? { gte: deadlineDate } : undefined,
type: type || { in: ['bounty', 'project', 'hackathon'] },
agentAccess: { in: ['AGENT_ALLOWED', 'AGENT_ONLY'] },
sponsor: {
Expand Down