feat: Implementation of bounty participation & submission backend logic#56
Conversation
📝 WalkthroughWalkthroughAdds an in-memory BountyStore and TypeScript participation models, extends ClaimingModel with 'milestone', introduces multiple Next.js App Router API routes for applications, submissions, and milestone participation (apply/join/submit/review/advance/select), and adds Vitest unit tests for store behaviors. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant API as "API Route\n(e.g., /bounties/[id]/join)"
participant Store as "BountyStore\n(lib/store.ts)"
Client->>API: POST /bounties/:id/(apply|join|submit) { body }
API->>Store: getBountyById(bountyId)
Store-->>API: bounty or null
alt bounty exists
API->>Store: get*ByBounty(bountyId)
Store-->>API: existing records
alt no duplicate
API->>Store: add*(newRecord)
Store-->>API: createdRecord
API-->>Client: 200 { data: createdRecord }
else duplicate
API-->>Client: 409 { error }
end
else not found
API-->>Client: 404 { error }
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Fix all issues with AI agents
In `@app/api/bounties/`[id]/apply/route.ts:
- Around line 11-30: Before constructing and adding the Application, verify the
bounty exists and that the applicant hasn't already applied: call the store
lookup used elsewhere (e.g., BountyStore.getBounty or BountyStore.findById) with
bountyId and return NextResponse.json({ error: 'Bounty not found' }, { status:
404 }) if absent; then check for an existing application for this
bounty/applicant pair using the same pattern as the join route (e.g.,
BountyStore.getApplications(bountyId) or BountyStore.hasApplication(bountyId,
applicantId)) and return NextResponse.json({ error: 'Application already exists'
}, { status: 409 }) if found; only then build the Application (id via
generateId(), fields applicantId, coverLetter, portfolioUrl, status,
submittedAt) and call BountyStore.addApplication(application).
In `@app/api/bounties/`[id]/join/route.ts:
- Around line 11-27: Before checking participations, fetch and validate the
bounty using BountyStore (e.g., call a getter like BountyStore.getById or
BountyStore.getBounty with bountyId) and ensure the returned bounty exists and
that bounty.claimingModel === 'milestone'; if the bounty is missing return a 404
JSON response and if the claimingModel is not 'milestone' return a 400/409 JSON
response. Place this validation above the existing
getMilestoneParticipationsByBounty call and only proceed to check/create
participations when the bounty passes these checks.
In `@app/api/bounties/`[id]/milestones/advance/route.ts:
- Around line 13-40: Validate the incoming action against the allowed set
('advance', 'complete', 'remove') before mutating participation or setting
lastUpdatedAt: check the destructured action from body and if it is not one of
the supported values return NextResponse.json({ error: 'Invalid action' }, {
status: 400 }); only then compute updates (currentMilestone, status and
lastUpdatedAt) and apply them via the existing participation/update flow (refer
to BountyStore.getMilestoneParticipationsByBounty, participation, and updates),
and ensure the handler does not set lastUpdatedAt or return 200 for unknown
actions.
In `@app/api/bounties/`[id]/submit/route.ts:
- Around line 11-30: Before calling BountyStore.addSubmission, verify the bounty
exists using the store (e.g., call BountyStore.getBounty(bountyId) or an
equivalent finder with the bountyId) and if it returns null/undefined return a
404 NextResponse.json({ error: 'Bounty not found' }, { status: 404 }); do not
create the Submission or call generateId() when the bounty is missing; keep the
rest of the submission creation and BountyStore.addSubmission call unchanged.
In `@lib/store.ts`:
- Around line 12-13: The code uses (global as any).bountyStore causing
no-explicit-any; declare a typed global property for the singleton (e.g. add a
declare global { var bountyStore?: BountyStoreData } block in this module) and
then replace uses of (global as any).bountyStore with globalThis.bountyStore so
you can initialize const globalStore: BountyStoreData = globalThis.bountyStore
|| new BountyStoreData(); and, in non-production, assign globalThis.bountyStore
= globalStore; to maintain type safety and satisfy the linter while referencing
the existing BountyStoreData and globalStore symbols.
🧹 Nitpick comments (9)
app/api/bounties/[id]/apply/route.ts (1)
33-35: Unusederrorvariable in catch block.The caught error is not logged or used, which makes debugging difficult. Consider logging the error for observability.
♻️ Proposed fix
- } catch (error) { + } catch (error) { + console.error('Failed to create application:', error); return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); }app/api/bounties/[id]/submit/route.ts (1)
34-36: Unusederrorvariable in catch block.Consider logging the error for debugging purposes, consistent with production observability needs.
♻️ Proposed fix
- } catch (error) { + } catch (error) { + console.error('Failed to create submission:', error); return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); }app/api/applications/[id]/review/route.ts (1)
31-33: Consider logging the error for observability.The caught error is discarded. For consistency across route handlers, consider logging it.
♻️ Proposed fix
- } catch (error) { + } catch (error) { + console.error('Failed to review application:', error); return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); }app/api/bounties/[id]/applications/route.ts (1)
4-11: Missing error handling.Unlike other route handlers in this PR, this GET handler lacks a try-catch block. If
BountyStore.getApplicationsByBountythrows unexpectedly, the error will propagate unhandled.♻️ Proposed fix to add error handling
export async function GET( request: Request, { params }: { params: Promise<{ id: string }> } ) { - const { id: bountyId } = await params; - const applications = BountyStore.getApplicationsByBounty(bountyId); - return NextResponse.json({ data: applications }); + try { + const { id: bountyId } = await params; + const applications = BountyStore.getApplicationsByBounty(bountyId); + return NextResponse.json({ data: applications }); + } catch (error) { + console.error('Failed to retrieve applications:', error); + return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); + } }app/api/submissions/[id]/select/route.ts (1)
32-34: Consider logging the error for observability.For consistency across route handlers, consider logging the caught error.
♻️ Proposed fix
- } catch (error) { + } catch (error) { + console.error('Failed to select submission:', error); return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); }lib/store.test.ts (4)
1-9: Tests rely on execution order and shared mutable state.The comment at lines 6-9 correctly identifies the issue but doesn't address it. The
beforeEachimport is unused. Tests like "should update application status" (line 28) depend on state created by the previous test, making them order-dependent and potentially flaky.Consider either:
- Adding a
reset()method to BountyStore and calling it inbeforeEach- Creating fresh entities within each test that needs them
♻️ Option 1: Add reset capability to store
In
lib/store.ts:// Add to BountyStore reset: () => { globalStore.applications = []; globalStore.submissions = []; globalStore.milestoneParticipations = []; }In
lib/store.test.ts:-import { describe, it, expect, beforeEach } from 'vitest'; +import { describe, it, expect, beforeEach } from 'vitest'; import { BountyStore } from './store'; import { Application, Submission, MilestoneParticipation } from '@/types/participation'; describe('BountyStore', () => { - // Note: Since BountyStore uses a global singleton, state might persist. - // Ideally we'd have a reset method, but for this basic verification we'll assume clean state or manage it. - // However, unit tests in Vitest usually run in isolation per file, but global state might persist if not reset. - // For now, let's just test distinct IDs. + beforeEach(() => { + BountyStore.reset(); + });
28-32: Test depends on prior test's side effects.This test assumes
app-1was created by the previous test. If tests run in isolation or in a different order, this will fail. Consider creating the application within this test or using the reset approach mentioned above.♻️ Make test self-contained
it('should update application status', () => { + const app: Application = { + id: 'app-update-test', + bountyId: 'b-1', + applicantId: 'u-1', + coverLetter: 'Hire me', + status: 'pending', + submittedAt: new Date().toISOString() + }; + BountyStore.addApplication(app); - const updated = BountyStore.updateApplication('app-1', { status: 'approved' }); + const updated = BountyStore.updateApplication('app-update-test', { status: 'approved' }); expect(updated?.status).toBe('approved'); - expect(BountyStore.getApplicationById('app-1')?.status).toBe('approved'); + expect(BountyStore.getApplicationById('app-update-test')?.status).toBe('approved'); });
49-52: Test depends on prior test's side effects.Same issue as the application update test - this assumes
sub-1exists from the previous test.
71-75: Test depends on prior test's side effects.Same issue - this test assumes
mp-1exists from the previous test.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@app/api/bounties/`[id]/milestones/advance/route.ts:
- Around line 34-39: Before setting updates for 'advance' or 'complete',
validate the current participation state: check participation.currentMilestone
and participation.status (and total allowed milestones via
participation.totalMilestones or bounty.milestones.length) and if the transition
is invalid (e.g., action === 'advance' when participation.status === 'completed'
or participation.currentMilestone >= totalMilestones, or action === 'complete'
when participation.status === 'completed' or no pending milestone) abort and
return a 409 response; otherwise proceed to set updates.currentMilestone =
participation.currentMilestone + 1 and updates.status = 'advanced' (for advance)
or updates.status = 'completed' (for complete).
In `@app/api/bounties/`[id]/submit/route.ts:
- Around line 21-35: The submit handler currently creates a Submission
unconditionally; add two guards: first, validate the bounty's claimingModel (use
BountyStore.getBountyById and compare bounty.claimingModel to the allowed
model(s), returning a 400/403 JSON error if not allowed), and second, prevent
duplicate submissions by the same contributor for the same bounty by checking
existing submissions via a BountyStore query (e.g.,
BountyStore.getSubmissionsByBountyId or a new
BountyStore.hasSubmissionForContributor) before calling generateId and
BountyStore.addSubmission, returning a 409/400 JSON error if a duplicate is
found.
🧹 Nitpick comments (2)
app/api/bounties/[id]/join/route.ts (1)
52-54: Log the error for observability.The caught error is discarded without logging, which makes debugging production issues difficult.
♻️ Suggested improvement
} catch (error) { + console.error('Failed to join bounty milestone:', error); return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); }app/api/bounties/[id]/submit/route.ts (1)
39-41: Consider logging errors for observability.The caught error is not logged, which makes debugging production issues harder. This is consistent with other routes in this PR, but as the codebase matures, consider adding structured logging.
♻️ Optional improvement
} catch (error) { + console.error('Submit route error:', error); return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); }
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@app/api/bounties/`[id]/milestones/advance/route.ts:
- Around line 30-36: Replace the unsafe `as any` casts and add a bounty
null-check: update types (e.g., MilestoneParticipation and Bounty) to include
totalMilestones and milestones so you can safely read
participation.totalMilestones and bounty.milestones without casting; in route.ts
(the logic around BountyStore.getBountyById, participation and the computed
totalMilestones) perform a null/undefined check on `bounty` and fall back to a
validated numeric total (or return a 404/error if bounty missing) before
allowing any advance logic so totalMilestones cannot be undefined and the guard
preventing advances past the final milestone is reliable.
🧹 Nitpick comments (4)
app/api/bounties/[id]/submit/route.ts (2)
13-19: Consider distinguishing JSON parse errors from internal errors.If
request.json()fails due to malformed JSON, it will be caught by the generic error handler and return a 500 status. For better client feedback, consider handling JSON parse failures explicitly with a 400 response.♻️ Proposed fix
try { - const body = await request.json(); + let body; + try { + body = await request.json(); + } catch { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } const { contributorId, content } = body;
52-54: Consider logging errors for observability.The caught
erroris unused, which means failures are silently swallowed. Logging the error would help with debugging and production observability.♻️ Proposed fix
} catch (error) { + console.error('Failed to create submission:', error); return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); }app/api/bounties/[id]/milestones/advance/route.ts (2)
3-3: Remove commented import.This commented import appears to be dead code. Either use it or remove it entirely.
-// import { MilestoneStatus } from '@/types/participation';
60-61: Log the caught error for debugging.The error is caught but discarded, which makes debugging production issues difficult.
} catch (error) { + console.error('Milestone advance error:', error); return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); }
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@types/bounty.ts`:
- Line 49: Replace the loose any[] on the Bounty type's milestones property with
a concrete Milestone interface and use it for typing (e.g., define interface
Milestone { /* id, title, amount, completed? etc. */ } and change milestones?:
any[] to milestones?: Milestone[]). Update any code that accesses milestone
fields (such as routes that compute totalMilestones) to use the new Milestone
shape, and export the Milestone interface if needed so callers can import it
consistently.
🧹 Nitpick comments (2)
app/api/bounties/[id]/milestones/advance/route.ts (2)
36-38: Useconstinstead oflet.The
updatesvariable is never reassigned; only its properties are modified. ESLint correctly flags this.🔧 Proposed fix
- let updates: Partial<typeof participation> = { + const updates: Partial<typeof participation> = { lastUpdatedAt: new Date().toISOString() };
40-40: Use nullish coalescing (??) instead of logical OR (||).If
participation.totalMilestonesis explicitly0, the||operator will treat it as falsy and fall back tobounty.milestones?.length. While0milestones is unlikely in practice, using??is semantically correct and guards against subtle bugs.🔧 Proposed fix
- const totalMilestones = participation.totalMilestones || bounty.milestones?.length; + const totalMilestones = participation.totalMilestones ?? bounty.milestones?.length;
|
@0xdevcollins |
Closes #52
Implementation of Bounty Participation & Submission APIs
Description
This PR implements the core backend infrastructure for the Bounty application system. It includes the data models, an in-memory test store, and the complete set of API routes to handle user interactions with bounties (joining, applying, submitting work, and milestone tracking).
Core Features
Data Models
Defined TypeScript interfaces for:
BountyApplicationSubmissionMilestoneParticipationState Management
Implemented
BountyStore(lib/store.ts) as a robust in-memory store for handling local development state without a database.REST APIs
Applications
POST /api/bounties/[id]/apply— Submit new applicationMilestones
POST /api/bounties/[id]/join— Start working on a milestone bountyPOST /api/bounties/[id]/milestones/advance— Progress to next milestoneSubmissions
POST /api/bounties/[id]/submit— Submit final workData Retrieval
Endpoints for fetching applications, submissions, and checking participation status.
Key Changes
New Types
types/participation.tstypes/bounty.tsStore Logic
BountyStorefor all participation models.Route Handlers
Verification Outcomes
Type Safety
anyinference issues in the store.Functionality
MilestoneParticipationrecords.Applicationrecords correctly.Submissioncreation.200,400,404,409).Checklist
Summary by CodeRabbit
New Features
Types
Chores
Tests
✏️ Tip: You can customize this high-level summary in your review settings.