Skip to content

Commit ef43668

Browse files
authored
Merge pull request #110 from sudo-robi/feature/bulk-operations
Add bulk verification endpoints
2 parents 2111462 + 194d08b commit ef43668

4 files changed

Lines changed: 228 additions & 4 deletions

File tree

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,16 @@ Rust-based smart contracts deployed on **Stellar Testnet** via Soroban. Features
3939
Express.js API server providing:
4040

4141
- **AI Work Verification** — Validates freelancer deliverables against project requirements using AI
42+
- **Bulk Verification Operations** — Batch verify, update, and delete verification records
4243
- **AI Invoice Generation** — Automated invoice creation for completed work
4344
- **Stellar Horizon Integration** — On-chain payment status and transaction lookups
4445

46+
### Bulk Verification Endpoints
47+
48+
- `POST /api/v1/verification/verify/batch`
49+
- `PATCH /api/v1/verification/batch`
50+
- `DELETE /api/v1/verification/batch`
51+
4552
## Features
4653

4754
- **Instant Payments** — Funds released immediately upon work approval via Soroban

backend/src/routes/verification.ts

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { Router } from 'express';
2-
import { verifyWork, getVerification } from '../services/verification.js';
2+
import {
3+
verifyWork,
4+
getVerification,
5+
updateVerification,
6+
deleteVerification,
7+
} from '../services/verification.js';
38
import { idempotency } from '../middleware/idempotency.js';
49

510
export const verificationRouter = Router();
@@ -22,6 +27,130 @@ verificationRouter.post('/verify', idempotency(), async (req, res) => {
2227
}
2328
});
2429

30+
// Bulk AI-powered work verification
31+
verificationRouter.post('/verify/batch', idempotency(), async (req, res) => {
32+
try {
33+
const { items } = req.body as {
34+
items?: Array<{ repositoryUrl?: string; milestoneDescription?: string; projectId?: string }>;
35+
};
36+
37+
if (!Array.isArray(items) || items.length === 0) {
38+
res.status(400).json({ message: 'Missing items for bulk verification' });
39+
return;
40+
}
41+
42+
const results = await Promise.all(
43+
items.map(async (item, index) => {
44+
if (!item?.repositoryUrl || !item?.milestoneDescription || !item?.projectId) {
45+
return { index, status: 'error', error: 'Missing required fields' };
46+
}
47+
48+
try {
49+
const data = await verifyWork({
50+
repositoryUrl: item.repositoryUrl,
51+
milestoneDescription: item.milestoneDescription,
52+
projectId: item.projectId,
53+
});
54+
return { index, status: 'success', data };
55+
} catch (error) {
56+
const message = error instanceof Error ? error.message : 'Verification failed';
57+
return { index, status: 'error', error: message };
58+
}
59+
})
60+
);
61+
62+
res.json({ results });
63+
} catch (error) {
64+
console.error('Bulk verification error:', error);
65+
res.status(500).json({ message: 'Bulk verification failed' });
66+
}
67+
});
68+
69+
// Bulk update verification results
70+
verificationRouter.patch('/batch', (req, res) => {
71+
try {
72+
const { items } = req.body as {
73+
items?: Array<{
74+
id?: string;
75+
status?: 'passed' | 'failed' | 'pending';
76+
score?: number;
77+
summary?: string;
78+
details?: string[];
79+
}>;
80+
};
81+
82+
if (!Array.isArray(items) || items.length === 0) {
83+
res.status(400).json({ message: 'Missing items for bulk update' });
84+
return;
85+
}
86+
87+
const results = items.map((item, index) => {
88+
if (!item?.id) {
89+
return { index, status: 'error', error: 'Missing verification id' };
90+
}
91+
92+
const hasUpdates =
93+
item.status !== undefined ||
94+
item.score !== undefined ||
95+
item.summary !== undefined ||
96+
item.details !== undefined;
97+
98+
if (!hasUpdates) {
99+
return { index, status: 'error', error: 'No update fields provided' };
100+
}
101+
102+
const updated = updateVerification({
103+
id: item.id,
104+
status: item.status,
105+
score: item.score,
106+
summary: item.summary,
107+
details: item.details,
108+
});
109+
110+
if (!updated) {
111+
return { index, status: 'error', error: 'Verification not found' };
112+
}
113+
114+
return { index, status: 'success', data: updated };
115+
});
116+
117+
const updatedCount = results.filter((result) => result.status === 'success').length;
118+
res.json({ results, updatedCount });
119+
} catch (error) {
120+
console.error('Bulk update error:', error);
121+
res.status(500).json({ message: 'Bulk update failed' });
122+
}
123+
});
124+
125+
// Bulk delete verification results
126+
verificationRouter.delete('/batch', (req, res) => {
127+
try {
128+
const { ids } = req.body as { ids?: string[] };
129+
130+
if (!Array.isArray(ids) || ids.length === 0) {
131+
res.status(400).json({ message: 'Missing ids for bulk delete' });
132+
return;
133+
}
134+
135+
const results = ids.map((id) => {
136+
if (!id) {
137+
return { id, status: 'error', error: 'Missing verification id' };
138+
}
139+
140+
const deleted = deleteVerification(id);
141+
return deleted
142+
? { id, status: 'deleted' }
143+
: { id, status: 'not_found' };
144+
});
145+
146+
const deletedCount = results.filter((result) => result.status === 'deleted').length;
147+
res.json({ results, deletedCount });
148+
} catch (error) {
149+
console.error('Bulk delete error:', error);
150+
res.status(500).json({ message: 'Bulk delete failed' });
151+
}
152+
});
153+
25154
// Get verification result by ID
26155
verificationRouter.get('/:id', async (req, res) => {
27156
try {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { describe, expect, it } from 'vitest';
2+
import {
3+
storeVerification,
4+
updateVerification,
5+
deleteVerification,
6+
getVerification,
7+
} from '../verification.js';
8+
9+
const baseVerification = {
10+
projectId: 'project-1',
11+
status: 'pending' as const,
12+
score: 50,
13+
summary: 'Initial review',
14+
details: ['Initial note'],
15+
verifiedAt: new Date('2024-01-01T00:00:00Z').toISOString(),
16+
};
17+
18+
describe('verification bulk helpers', () => {
19+
it('updates existing verification entries', () => {
20+
const id = 'ver_update_test';
21+
storeVerification({ ...baseVerification, id });
22+
23+
const updated = updateVerification({ id, status: 'passed', score: 90 });
24+
25+
expect(updated?.status).toBe('passed');
26+
expect(updated?.score).toBe(90);
27+
});
28+
29+
it('deletes verification entries', async () => {
30+
const id = 'ver_delete_test';
31+
storeVerification({ ...baseVerification, id });
32+
33+
const deleted = deleteVerification(id);
34+
35+
expect(deleted).toBe(true);
36+
await expect(getVerification(id)).resolves.toBeUndefined();
37+
});
38+
});

backend/src/services/verification.ts

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
import OpenAI from 'openai';
22

3-
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
3+
let openaiClient: OpenAI | null = null;
4+
5+
const getOpenAIClient = () => {
6+
const apiKey = process.env.OPENAI_API_KEY;
7+
if (!apiKey) {
8+
throw new Error(
9+
'The OPENAI_API_KEY environment variable is missing or empty; provide it to run verification.'
10+
);
11+
}
12+
13+
if (!openaiClient) {
14+
openaiClient = new OpenAI({ apiKey });
15+
}
16+
17+
return openaiClient;
18+
};
419

520
interface VerificationRequest {
621
repositoryUrl: string;
@@ -18,6 +33,14 @@ interface VerificationResult {
1833
verifiedAt: string;
1934
}
2035

36+
export type VerificationUpdate = {
37+
id: string;
38+
status?: 'passed' | 'failed' | 'pending';
39+
score?: number;
40+
summary?: string;
41+
details?: string[];
42+
};
43+
2144
// In-memory store (replace with DB in production)
2245
const verifications = new Map<string, VerificationResult>();
2346

@@ -27,7 +50,7 @@ export async function verifyWork(request: VerificationRequest): Promise<Verifica
2750
// TODO: Fetch actual repo contents via GitHub API
2851
// For now, use AI to generate a verification assessment
2952

30-
const completion = await openai.chat.completions.create({
53+
const completion = await getOpenAIClient().chat.completions.create({
3154
model: 'gpt-4o-mini',
3255
messages: [
3356
{
@@ -55,10 +78,37 @@ export async function verifyWork(request: VerificationRequest): Promise<Verifica
5578
verifiedAt: new Date().toISOString(),
5679
};
5780

58-
verifications.set(id, result);
81+
storeVerification(result);
5982
return result;
6083
}
6184

85+
export function storeVerification(result: VerificationResult): void {
86+
verifications.set(result.id, result);
87+
}
88+
6289
export async function getVerification(id: string): Promise<VerificationResult | undefined> {
6390
return verifications.get(id);
6491
}
92+
93+
export function updateVerification(update: VerificationUpdate): VerificationResult | undefined {
94+
const current = verifications.get(update.id);
95+
if (!current) {
96+
return undefined;
97+
}
98+
99+
const updated: VerificationResult = {
100+
...current,
101+
status: update.status ?? current.status,
102+
score: update.score ?? current.score,
103+
summary: update.summary ?? current.summary,
104+
details: update.details ?? current.details,
105+
verifiedAt: new Date().toISOString(),
106+
};
107+
108+
verifications.set(update.id, updated);
109+
return updated;
110+
}
111+
112+
export function deleteVerification(id: string): boolean {
113+
return verifications.delete(id);
114+
}

0 commit comments

Comments
 (0)