Skip to content

feat: Implementation of bounty participation & submission backend logic#56

Merged
0xdevcollins merged 4 commits intoboundlessfi:mainfrom
Michaelkingsdev:bounty-participation-submission-logic
Jan 29, 2026
Merged

feat: Implementation of bounty participation & submission backend logic#56
0xdevcollins merged 4 commits intoboundlessfi:mainfrom
Michaelkingsdev:bounty-participation-submission-logic

Conversation

@Michaelkingsdev
Copy link
Contributor

@Michaelkingsdev Michaelkingsdev commented Jan 29, 2026

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:

  • Bounty
  • Application
  • Submission
  • MilestoneParticipation

State 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 application

Milestones

  • POST /api/bounties/[id]/join — Start working on a milestone bounty
  • POST /api/bounties/[id]/milestones/advance — Progress to next milestone

Submissions

  • POST /api/bounties/[id]/submit — Submit final work

Data Retrieval

Endpoints for fetching applications, submissions, and checking participation status.

Key Changes

  • New Types

    • Added types/participation.ts
    • Updated types/bounty.ts
  • Store Logic

    • Added CRUD operations in BountyStore for all participation models.
  • Route Handlers

    • Created secure API routes with basic validation (checking existence, duplicates, etc.).

Verification Outcomes

Type Safety

  • Fixed implicit any inference issues in the store.
  • Full type safety across API handlers.

Functionality

  • Verified join flow creates correct MilestoneParticipation records.
  • Verified apply flow stores Application records correctly.
  • Verified submit flow handles Submission creation.
  • API routes return appropriate HTTP status codes (200, 400, 404, 409).

Checklist

  • API Routes implemented
  • Data Store implemented
  • TypeScript types defined
  • Basic error handling included
  • Fixed type inference issues

Summary by CodeRabbit

  • New Features

    • Milestone-based bounty model with join, progress (advance/complete), and participation tracking
    • Application flow: submit applications and receive review (approved/rejected) with feedback and timestamps
    • Submission flow: submit work and mark submissions as accepted/rejected with feedback and timestamps
    • Endpoints to list applications and submissions per bounty
  • Types

    • Added domain types for applications, submissions, and milestone participations
  • Chores

    • In-memory store for bounties, applications, submissions, and milestones (dev/mock)
  • Tests

    • Unit tests covering applications, submissions, and milestone workflows

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 29, 2026

📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
Type definitions
types/participation.ts, types/bounty.ts
Add Application/Submission/Milestone types and status unions; extend ClaimingModel to include 'milestone' and optional milestones on Bounty.
In-memory store + tests
lib/store.ts, lib/store.test.ts
Introduce singleton BountyStore with add/get/update methods for bounties, applications, submissions, and milestone participations; add Vitest unit tests covering add/retrieve/update flows.
Application routes
app/api/bounties/[id]/apply/route.ts, app/api/applications/[id]/review/route.ts, app/api/bounties/[id]/applications/route.ts
POST to submit applications, POST to review (approve/reject with feedback), GET to list applications by bounty; includes validation, duplicate checks, and error responses.
Submission routes
app/api/bounties/[id]/submit/route.ts, app/api/bounties/[id]/submissions/route.ts, app/api/submissions/[id]/select/route.ts
POST to create submissions with claiming-model checks and duplicate prevention, GET to list submissions, POST to accept/reject submissions with feedback and reviewedAt.
Milestone routes
app/api/bounties/[id]/join/route.ts, app/api/bounties/[id]/milestones/advance/route.ts
POST to join milestone participation (duplicate check) and POST to advance/complete milestone participation with status transitions, validations, and update persistence.

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐇
I hopped through code and planted a store,
Applications, submissions, and milestones galore.
Types and routes I stitched with care,
Tests to prove the logic’s there.
Hoppy build — go deploy and explore!

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main change: implementation of backend logic for bounty participation and submission workflows.
Linked Issues check ✅ Passed The PR implements Apply (Model 2) and Join Milestone (Model 4) workflows with validation, duplicate checks, and error handling as required by issue #51.
Out of Scope Changes check ✅ Passed All changes are directly related to bounty participation and submission backend logic. No extraneous modifications detected.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: Unused error variable 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: Unused error variable 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.getApplicationsByBounty throws 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 beforeEach import 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:

  1. Adding a reset() method to BountyStore and calling it in beforeEach
  2. 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-1 was 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-1 exists from the previous test.


71-75: Test depends on prior test's side effects.

Same issue - this test assumes mp-1 exists from the previous test.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 });
     }

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 error is 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 });
     }

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: Use const instead of let.

The updates variable 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.totalMilestones is explicitly 0, the || operator will treat it as falsy and fall back to bounty.milestones?.length. While 0 milestones 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;

@Michaelkingsdev
Copy link
Contributor Author

@0xdevcollins
Kindly review

@0xdevcollins 0xdevcollins merged commit 31fa6e6 into boundlessfi:main Jan 29, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Application, Competition, and Milestone Submission Logic

2 participants